封面图片

Addison-Wesley 签名系列

The Addison-Wesley Signature Series

 

Addison-Wesley 签名系列为读者提供有关计算机专业人士现代技术最新趋势的实用和权威信息。该系列基于一个简单的前提:伟大的书籍来自伟大的作者。该系列中的书籍由专家顾问亲自挑选,他们本身就是世界级的作家。这些专家很自豪地在封面上签名,他们的签名确保这些思想领袖与作者密切合作,确定主题范围、书籍范围、关键内容和整体独特性。专家签名还象征着对读者的承诺:您正在阅读未来的经典。

The Addison-Wesley Signature Series provides readers with practical and authoritative information on the latest trends in modern technology for computer professionals. The series is based on one simple premise: great books come from great authors. Books in the series are personally chosen by expert advisors, world-class authors in their own right. These experts are proud to put their signatures on the covers, and their signatures ensure that these thought leaders have worked closely with authors to define topic coverage, book scope, critical content, and overall uniqueness. The expert signatures also symbolize a promise to our readers: you are reading a future classic.

Addison-Wesley 签名系列

The Addison-Wesley Signature Series

签名者:肯特·贝克 (Kent Beck) 和马丁·福勒 (Martin Fowler)

Signers: Kent Beck and Martin Fowler

Kent Beck率先推出了以人为本的技术,如 JUnit、极限编程和软件开发模式。Kent 致力于通过做好事来帮助团队取得成功 — 找到一种能够同时满足经济、美学、情感和实际约束的软件开发风格。他的书专注于触动软件创建者和用户的生活。

Kent Beck has pioneered people-oriented technologies like JUnit, Extreme Programming, and patterns for software development. Kent is interested in helping teams do well by doing good—finding a style of software development that simultaneously satisfies economic, aesthetic, emotional, and practical constraints. His books focus on touching the lives of the creators and users of software.

Martin Fowler是企业应用程序中对象技术的先驱。他关注的重点是如何设计出好的软件。他专注于挖掘如何构建未来可长期使用的企业软件的核心。他感兴趣的是深入研究技术细节背后的模式、实践和原则,这些书应该在十年后仍然有用。Martin 的标准是,这些书是他希望自己也能写的。

Martin Fowler has been a pioneer of object technology in enterprise applications. His central concern is how to design software well. He focuses on getting to the heart of how to build enterprise software that will last well into the future. He is interested in looking behind the specifics of technologies to the patterns, practices, and principles that last for many years; these books should be usable a decade from now. Martin's criterion is that these are books he wished he could write.

图像

欲了解更多信息,请访问该系列网站:www.awprofessional.com

For more information, check out the series web site at www.awprofessional.com

xUnit 测试模式

xUnit Test Patterns

 

重构测试代码

Refactoring Test Code

 

杰拉德·梅萨罗斯

Gerard Meszaros

 

图像

新泽西州上萨德尔河 • 波士顿 • 印第安纳波利斯 • 旧金山

纽约 • 多伦多 • 蒙特利尔 • 伦敦 • 慕尼黑 • 巴黎 • 马德里

开普敦 • 悉尼 • 东京 • 新加坡 • 墨西哥城

Upper Saddle River, NJ • Boston • Indianapolis • San Francisco

New York • Toronto • Montreal • London • Munich • Paris • Madrid

Capetown • Sydney • Tokyo • Singapore • Mexico City

制造商和销售商用来区分其产品的许多名称均已声明为商标。本书中出现这些名称时,如果出版商知道商标声明,则这些名称将以首字母大写或全部大写形式印刷。

Many of the designations used by manufacturers and sellers to distinguish their products are claimed as trademarks. Where those designations appear in this book, and the publisher was aware of a trademark claim, the designations have been printed with initial capital letters or in all capitals.

作者和出版商在编写本书时已尽心尽力,但不作任何明示或暗示的保证,也不对错误或遗漏承担任何责任。对于因使用本文所含信息或程序而导致的或与之相关的偶然或间接损失,我们不承担任何责任。

The author and publisher have taken care in the preparation of this book, but make no expressed or implied warranty of any kind and assume no responsibility for errors or omissions. No liability is assumed for incidental or consequential damages in connection with or arising out of the use of the information or programs contained herein.

出版商在批量购买或特价销售时提供这本书的超值折扣,可能包括电子版和/或定制封面和内容,这些内容特定于您的业务、培训目标、营销重点和品牌兴趣。欲了解更多信息,请联系:

The publisher offers excellent discounts on this book when ordered in quantity for bulk purchases or special sales, which may include electronic versions and/or custom covers and content particular to your business, training goals, marketing focus, and branding interests. For more information, please contact:

美国企业和政府销售

(800) 382-3419

corpsales@pearsontechgroup.com

U.S. Corporate and Government Sales

(800) 382-3419

corpsales@pearsontechgroup.com

 

对于美国境外的销售,请联系:

For sales outside the United States please contact:

国际销售

international@pearsoned.com

International Sales

international@pearsoned.com

 

请访问我们的网站:informit.com/aw

Visit us on the Web: informit.com/aw

美国国会图书馆出版品目錄數據

Library of Congress Cataloging-in-Publication Data

Meszaros,Gerard。XUnit



  测试模式:重构测试代码 / Gerard Meszaros。p

        . cm。

  包括参考书目和索引。ISBN

  -13:978-0-13-149505-0(精装本:alk. paper)

  ISBN-10:0-13-149505-4

 1. 软件模式。2. 计算机软件 - 测试。I. 标题。QA76.76.P37M49

  2007

  005.1—dc22

                                                                                2006103488

Meszaros,  Gerard.



  XUnit  test  patterns  :  refactoring  test  code  /  Gerard  Meszaros.

        p.  cm.

  Includes  bibliographical  references  and  index.

  ISBN-13:  978-0-13-149505-0  (hardback  :  alk.  paper)

  ISBN-10:  0-13-149505-4

 1.  Software  patterns.  2.  Computer  software—Testing.  I.  Title.

  QA76.76.P37M49  2007

  005.1—dc22

                                                                                2006103488

版权所有 © 2007 Pearson Education, Inc.

Copyright © 2007 Pearson Education, Inc.

保留所有权利。印刷于美国。本出版物受版权保护,任何禁止复制、存储在检索系统中或以任何形式或任何手段(电子、机械、影印、录制或类似方式)传输之前,必须获得出版商的许可。有关许可的信息,请写信至:

All rights reserved. Printed in the United States of America. This publication is protected by copyright, and permission must be obtained from the publisher prior to any prohibited reproduction, storage in a retrieval system, or transmission in any form or by any means, electronic, mechanical, photocopying, recording, or likewise. For information regarding permissions, write to:

Pearson Education, Inc.

权利与合同部

501 Boylston Street, Suite 900

Boston, MA 02116

传真:(617) 671-3447

Pearson Education, Inc.

Rights and Contracts Department

501 Boylston Street, Suite 900

Boston, MA 02116

Fax: (617) 671-3447

 

国际标准书号 13:978-0-13-149505-0

ISBN 13: 978-0-13-149505-0

ISBN 10:0-13-149505-4

ISBN 10:         0-13-149505-4

文本由美国马萨诸塞州韦斯特福德的 Courier 公司使用再生纸印刷。

Text printed in the United States on recycled paper at Courier in Westford, Massachusetts.

第三次印刷,2009 年 2 月

Third printing, February 2009

本书献给 Denis Clelland,他于 1995 年将我从 Nortel 招至 ClearStream Consulting 工作,并因此让我有机会积累了创作本书的经验。遗憾的是,Denis 于 2006 年 4 月 27 日去世,当时我正在完成第二稿

This book is dedicated to the memory of Denis Clelland, who recruited me away from Nortel in 1995 to work at ClearStream Consulting and thereby gave me the opportunity to have the experiences that led to this book. Sadly, Denis passed away on April 27, 2006, while I was finalizing the second draft.

内容

Contents

 

模式语言的视觉摘要

Visual Summary of the Pattern Language

前言

Foreword

前言

Preface

致谢

Acknowledgments

介绍

Introduction

重构测试

Refactoring a Test

第一部分 叙述

PART I. The Narratives

 

第 1 章 简要介绍

Chapter 1. A Brief Tour

关于本章

About This Chapter

最简单的可能有效的测试自动化策略

The Simplest Test Automation Strategy That Could Possibly Work

开发过程

Development Process

客户测试

Customer Tests

单元测试

Unit Tests

可测试性设计

Design for Testability

测试组织

Test Organization

下一步是什么?

What's Next?

第 2 章 测试气味

Chapter 2. Test Smells

关于本章

About This Chapter

测试气味简介

An Introduction to Test Smells

什么是测试气味?

What's a Test Smell?

测试气味的种类

Kinds of Test Smells

如何处理气味?

What to Do about Smells?

气味目录

A Catalog of Smells

项目有异味

The Project Smells

行为气味

The Behavior Smells

代码异味

The Code Smells

下一步是什么?

What's Next?

第 3 章 测试自动化的目标

Chapter 3. Goals of Test Automation

关于本章

About This Chapter

为什么要测试?

Why Test?

测试自动化的经济学

Economics of Test Automation

测试自动化的目标

Goals of Test Automation

测试应该帮助我们提高质量

Tests Should Help Us Improve Quality

测试应该帮助我们理解 SUT

Tests Should Help Us Understand the SUT

测试应该减少(而不是引入)风险

Tests Should Reduce (and Not Introduce) Risk

测试应该易于运行

Tests Should Be Easy to Run

测试应该易于编写和维护

Tests Should Be Easy to Write and Maintain

随着系统的发展,测试应该需要最少的维护

Tests Should Require Minimal Maintenance as the System Evolves Around Them

下一步是什么?

What's Next?

第 4 章 测试自动化的哲学

Chapter 4. Philosophy of Test Automation

关于本章

About This Chapter

为什么哲学很重要?

Why Is Philosophy Important?

一些哲学差异

Some Philosophical Differences

先测试还是最后测试?

Test First or Last?

测试还是例子?

Tests or Examples?

逐个测试还是一次性测试全部?

Test-by-Test or Test All-at-Once?

由外而内还是由内而外?

Outside-In or Inside-Out?

状态或行为验证?

State or Behavior Verification?

提前设计夹具还是逐个测试?

Fixture Design Upfront or Test-by-Test?

当哲学观点不同时

When Philosophies Differ

我的哲学

My Philosophy

下一步是什么?

What's Next?

第 5 章 测试自动化原理

Chapter 5. Principles of Test Automation

关于本章

About This Chapter

原则

The Principles

下一步是什么?

What's Next?

第六章 测试自动化策略

Chapter 6. Test Automation Strategy

关于本章

About This Chapter

什么是战略?

What's Strategic?

我们应该实现哪些类型的测试自动化?

Which Kinds of Tests Should We Automate?

功能测试

Per-Functionality Tests

跨功能测试

Cross-Functional Tests

我们使用哪些工具来自动化哪些测试?

Which Tools Do We Use to Automate Which Tests?

测试自动化的方法和手段

Test Automation Ways and Means

介绍 xUnit

Introducing xUnit

xUnit 的最佳点

The xUnit Sweet Spot

我们使用哪种测试装置策略?

Which Test Fixture Strategy Do We Use?

什么是夹具?

What Is a Fixture?

主要赛事策略

Major Fixture Strategies

临时新鲜装置

Transient Fresh Fixtures

持续更新赛事

Persistent Fresh Fixtures

共享装置策略

Shared Fixture Strategies

我们如何确保可测试性?

How Do We Ensure Testability?

最后测试——后果自负

Test Last—at Your Peril

可测试性设计—前期

Design for Testability—Upfront

测试驱动的可测试性

Test-Driven Testability

控制点和观察点

Control Points and Observation Points

交互风格和可测试性模式

Interaction Styles and Testability Patterns

划分并测试

Divide and Test

下一步是什么?

What's Next?

第 7 章 xUnit 基础

Chapter 7. xUnit Basics

关于本章

About This Chapter

xUnit 简介

An Introduction to xUnit

共同特征

Common Features

最低限度

The Bare Minimum

定义测试

Defining Tests

什么是 Fixture?

What's a Fixture?

定义测试套件

Defining Suites of Tests

运行测试

Running Tests

测试结果

Test Results

在 xUnit 封面下

Under the xUnit Covers

测试命令

Test Commands

测试套件对象

Test Suite Objects

程序世界中的 xUnit

xUnit in the Procedural World

下一步是什么?

What's Next?

第 8 章 瞬态夹具管理

Chapter 8. Transient Fixture Management

关于本章

About This Chapter

测试夹具术语

Test Fixture Terminology

什么是夹具?

What Is a Fixture?

什么是新鲜装置?

What Is a Fresh Fixture?

什么是瞬态新鲜装置?

What Is a Transient Fresh Fixture?

打造新鲜装置

Building Fresh Fixtures

在线夹具设置

In-line Fixture Setup

委托装置设置

Delegated Fixture Setup

隐式夹具设置

Implicit Fixture Setup

混合夹具设置

Hybrid Fixture Setup

拆除临时的新鲜装置

Tearing Down Transient Fresh Fixtures

下一步是什么?

What's Next?

第 9 章 持久性 Fixture 管理

Chapter 9. Persistent Fixture Management

关于本章

About This Chapter

管理持久的新鲜装置

Managing Persistent Fresh Fixtures

什么使得 Fixtures 持久化?

What Makes Fixtures Persistent?

持续使用新鲜装置引起的问题

Issues Caused by Persistent Fresh Fixtures

拆除持久的新鲜装置

Tearing Down Persistent Fresh Fixtures

避免拆卸

Avoiding the Need for Teardown

处理缓慢的测试

Dealing with Slow Tests

管理共享装置

Managing Shared Fixtures

访问共享装置

Accessing Shared Fixtures

触发共享装置构造

Triggering Shared Fixture Construction

下一步是什么?

What's Next?

第十章 结果验证

Chapter 10. Result Verification

关于本章

About This Chapter

让测试自我检查

Making Tests Self-Checking

验证状态还是行为?

Verify State or Behavior?

州验证

State Verification

使用内置断言

Using Built-in Assertions

增量断言

Delta Assertions

外部结果验证

External Result Verification

验证行为

Verifying Behavior

程序行为验证

Procedural Behavior Verification

预期行为规范

Expected Behavior Specification

减少测试代码重复

Reducing Test Code Duplication

预期对象

Expected Objects

自定义断言

Custom Assertions

结果描述验证方法

Outcome-Describing Verification Method

参数化和数据驱动测试

Parameterized and Data-Driven Tests

避免条件测试逻辑

Avoiding Conditional Test Logic

消除“if”语句

Eliminating "if" Statements

消除环路

Eliminating Loops

其他技术

Other Techniques

从外向内逆向工作

Working Backward, Outside-In

使用测试驱动开发编写测试实用程序方法

Using Test-Driven Development to Write Test Utility Methods

可重复使用的验证逻辑放在哪里?

Where to Put Reusable Verification Logic?

下一步是什么?

What's Next?

第 11 章 使用测试替身

Chapter 11. Using Test Doubles

关于本章

About This Chapter

什么是间接输入和输出?

What Are Indirect Inputs and Outputs?

为什么我们关心间接投入?

Why Do We Care about Indirect Inputs?

为什么我们关心间接产出?

Why Do We Care about Indirect Outputs?

我们如何控制间接投入?

How Do We Control Indirect Inputs?

我们如何验证间接输出?

How Do We Verify Indirect Outputs?

使用 Double 进行测试

Testing with Doubles

测试替身的类型

Types of Test Doubles

提供测试替身

Providing the Test Double

配置测试替身

Configuring the Test Double

安装测试替身

Installing the Test Double

测试替身的其他用途

Other Uses of Test Doubles

内窥镜检查

Endoscopic Testing

需求驱动开发

Need-Driven Development

加快夹具设置速度

Speeding Up Fixture Setup

加速测试执行

Speeding Up Test Execution

其他考虑因素

Other Considerations

下一步是什么?

What's Next?

第 12 章组织我们的测试

Chapter 12. Organizing Our Tests

关于本章

About This Chapter

基本 xUnit 机制

Basic xUnit Mechanisms

合适规模的测试方法

Right-Sizing Test Methods

测试方法和测试用例类

Test Methods and Testcase Classes

每个类的测试用例类

Testcase Class per Class

每个功能的测试用例类

Testcase Class per Feature

每个装置的测试用例类

Testcase Class per Fixture

选择测试方法组织策略

Choosing a Test Method Organization Strategy

测试命名约定

Test Naming Conventions

组织测试套件

Organizing Test Suites

运行测试组

Running Groups of Tests

运行单个测试

Running a Single Test

测试代码重用

Test Code Reuse

测试实用程序方法位置

Test Utility Method Locations

测试用例的继承和重用

TestCase Inheritance and Reuse

测试文件组织

Test File Organization

内置自检

Built-in Self-Test

测试包

Test Packages

测试依赖项

Test Dependencies

下一步是什么?

What's Next?

第 13 章 使用数据库进行测试

Chapter 13. Testing with Databases

关于本章

About This Chapter

使用数据库进行测试

Testing with Databases

为什么要使用数据库进行测试?

Why Test with Databases?

数据库问题

Issues with Databases

不使用数据库进行测试

Testing without Databases

测试数据库

Testing the Database

测试存储过程

Testing Stored Procedures

测试数据访问层

Testing the Data Access Layer

确保开发人员的独立性

Ensuring Developer Independence

使用数据库进行测试(再次!)

Testing with Databases (Again!)

下一步是什么?

What's Next?

第 14 章 有效测试自动化的路线图

Chapter 14. A Roadmap to Effective Test Automation

关于本章

About This Chapter

测试自动化难度

Test Automation Difficulty

高度可维护的自动化测试路线图

Roadmap to Highly Maintainable Automated Tests

练习快乐路径代码

Exercise the Happy Path Code

验证快乐路径的直接输出

Verify Direct Outputs of the Happy Path

验证替代路径

Verify Alternative Paths

验证间接输出行为

Verify Indirect Output Behavior

优化测试执行和维护

Optimize Test Execution and Maintenance

下一步是什么?

What's Next?

第二部分 测试的味道

PART II. The Test Smells

 

第 15 章 代码异味

Chapter 15. Code Smells

模糊测试

Obscure Test

条件测试逻辑

Conditional Test Logic

难以测试的代码

Hard-to-Test Code

测试代码重复

Test Code Duplication

生产中的测试逻辑

Test Logic in Production

第 16 章 行为气味

Chapter 16. Behavior Smells

断言轮盘

Assertion Roulette

不稳定的测试

Erratic Test

易碎测试

Fragile Test

频繁调试

Frequent Debugging

人工干预

Manual Intervention

缓慢的测试

Slow Tests

第 17 章 项目异味

Chapter 17. Project Smells

有缺陷的测试

Buggy Tests

开发人员不编写测试

Developers Not Writing Tests

测试维护成本高

High Test Maintenance Cost

生产错误

Production Bugs

第三部分 模式

PART III. The Patterns

 

第 18 章 测试策略模式

Chapter 18. Test Strategy Patterns

录制测试

Recorded Test

脚本测试

Scripted Test

数据驱动测试

Data-Driven Test

测试自动化框架

Test Automation Framework

最小装置

Minimal Fixture

标准夹具

Standard Fixture

新鲜装置

Fresh Fixture

共享装置

Shared Fixture

后门操纵

Back Door Manipulation

层测试

Layer Test

第 19 章 xUnit 基础模式

Chapter 19. xUnit Basics Patterns

测试方法

Test Method

四相测试

Four-Phase Test

断言方法

Assertion Method

断言消息

Assertion Message

测试用例类

Testcase Class

测试运行器

Test Runner

测试用例对象

Testcase Object

测试套件对象

Test Suite Object

测试发现

Test Discovery

测试枚举

Test Enumeration

测试选择

Test Selection

第 20 章 Fixture 设置模式

Chapter 20. Fixture Setup Patterns

在线设置

In-line Setup

委派设置

Delegated Setup

创建方法

Creation Method

隐式设置

Implicit Setup

预制装置

Prebuilt Fixture

惰性设置

Lazy Setup

套房固定装置设置

Suite Fixture Setup

安装装饰器

Setup Decorator

链式测试

Chained Tests

第21章结果验证模式

Chapter 21. Result Verification Patterns

州验证

State Verification

行为验证

Behavior Verification

自定义断言

Custom Assertion

增量断言

Delta Assertion

保护断言

Guard Assertion

未完成的测试断言

Unfinished Test Assertion

第 22 章 Fixture 拆卸模式

Chapter 22. Fixture Teardown Patterns

垃圾收集拆卸

Garbage-Collected Teardown

自动拆卸

Automated Teardown

在线拆卸

In-line Teardown

隐式拆卸

Implicit Teardown

第 23 章 测试双重模式

Chapter 23. Test Double Patterns

测试替身

Test Double

测试桩

Test Stub

测试间谍

Test Spy

模拟对象

Mock Object

假物体

Fake Object

可配置测试替身

Configurable Test Double

硬编码测试替身

Hard-Coded Test Double

测试专用子类

Test-Specific Subclass

第 24 章 测试组织模式

Chapter 24. Test Organization Patterns

命名测试套件

Named Test Suite

测试实用程序方法

Test Utility Method

参数化测试

Parameterized Test

每个类的测试用例类

Testcase Class per Class

每个功能的测试用例类

Testcase Class per Feature

每个装置的测试用例类

Testcase Class per Fixture

测试用例超类

Testcase Superclass

测试助手

Test Helper

第 25 章 数据库模式

Chapter 25. Database Patterns

数据库沙箱

Database Sandbox

存储过程测试

Stored Procedure Test

表截断拆除

Table Truncation Teardown

事务回滚拆除

Transaction Rollback Teardown

第 26 章可测试性设计模式

Chapter 26. Design-for-Testability Patterns

依赖注入

Dependency Injection

依赖项查找

Dependency Lookup

卑微的物体

Humble Object

测试钩

Test Hook

第 27 章 价值模式

Chapter 27. Value Patterns

文字值

Literal Value

派生值

Derived Value

创造价值

Generated Value

虚拟对象

Dummy Object

第四部分 附录

PART IV. Appendixes

 

附录 A. 测试重构

Appendix A. Test Refactorings

附录 B. xUnit 术语

Appendix B. xUnit Terminology

附录 C. xUnit 系列成员

Appendix C. xUnit Family Members

附录 D. 工具

Appendix D. Tools

附录 E. 目标和原则

Appendix E. Goals and Principles

附录 F. 气味、别名和原因

Appendix F. Smells, Aliases, and Causes

附录 G. 模式、别名和变体

Appendix G. Patterns, Aliases, and Variations

                词汇表

                Glossary

                参考

                References

                指数

                Index

模式语言的视觉摘要

Visual Summary of the Pattern Language

 

目标、原则和气味

Goals, Principles, and Smells

图像

图像

模式

The Patterns

图像

前言

Foreword

 

如果您访问junit.org,您会看到我的一句话:“在软件开发领域,从来没有如此少的代码行能带来如此多的成果。”JUnit 一直被批评为微不足道的东西,任何有理智的程序员都可以在一个周末内完成。这是真的,但完全没有抓住要点。JUnit 之所以重要,并值得丘吉尔式的模仿,是因为这个小工具的存在对于许多程序员的根本转变至关重要:测试已成为编程的前沿和核心部分。人们以前就提倡过它,但 JUnit 比其他任何东西都更能使它成为现实。

If you go to junit.org, you'll see a quote from me: "Never in the field of software development have so many owed so much to so few lines of code." JUnit has been criticized as a minor thing, something any reasonable programmer could produce in a weekend. This is true, but utterly misses the point. The reason JUnit is important, and deserves the Churchillian knock-off, is that the presence of this tiny tool has been essential to a fundamental shift for many programmers: Testing has moved to a front and central part of programming. People have advocated it before, but JUnit made it happen more than anything else.

当然,它不仅仅是 JUnit。JUnit 的移植版本已经为许多编程语言编写。这个松散的工具系列,通常称为 xUnit 工具,已经远远超出了它的 Java 根源。(当然,它的根源并不在 Java 中——Kent Beck 多年前就为 Smalltalk 编写了这段代码。)

It's more than just JUnit, of course. Ports of JUnit have been written for lots of programming languages. This loose family of tools, often referred to as xUnit tools, has spread far beyond its java roots. (And of course the roots weren't really in Java—Kent Beck wrote this code for Smalltalk years before.)

xUnit 工具,更重要的是它们的理念,为编程团队提供了巨大的机会 - 编写强大的回归测试套件的机会,使团队能够以更小的风险对代码库进行重大更改;通过测试驱动开发重新思考设计过程的机会。

xUnit tools, and more importantly their philosophy, offer up huge opportunities to programming teams—the opportunity to write powerful regression test suites that enable teams to make drastic changes to a code-base with far less risk; the opportunity to re-think the design process with Test Driven Development.

但这些机会也带来了新的问题和新技术。与任何工具一样,xUnit 系列既可以用得好,也可以用得不好。有思想的人已经想出了使用 xUnit 的各种方法,以有效地组织测试和数据。就像早期的对象一样,真正使用这些工具的大部分知识隐藏在其熟练用户的头脑中。没有这些隐藏的知识,您真的无法获得全部好处。

But with these opportunities come new problems and new techniques. Like any tool, the xUnit family can be used well or badly. Thoughtful people have figured out various ways to use xUnit, to organize the tests and data effectively. Like the early days of objects, much of the knowledge to really use the tools is hidden in the heads of its skilled users. Without this hidden knowledge you really can't reap the full benefits.

大约二十年前,面向对象社区的人们意识到了对象的这个问题,并开始制定解决方案。答案是以模式的形式描述它们的隐藏知识。Gerard Meszaros 是这样做的先驱之一。当我第一次开始探索模式时,Gerard 是我学习的领导者之一。与模式世界中的许多人一样,Gerard 也是极限编程的早期采用者,因此从最早的时候就开始使用 xUnit 工具。因此,他应该承担以模式形式捕获这些专业知识的任务,这是完全合乎逻辑的。

It was nearly twenty years ago when people in the object-oriented community realized this problem for objects and began to formulate an answer. The answer was to describe their hidden knowledge in the form of patterns. Gerard Meszaros was one of the pioneers in doing this. When I first started exploring patterns, Gerard was one of the leaders that I learned from. Like many in the patterns world, Gerard also was an early adopter of eXtreme Programming, and thus worked with xUnit tools from the earliest days. So it's entirely logical that he should have taken on the task of capturing that expert knowledge in the form of patterns.

自从我第一次听说这个项目以来,我就一直对它感到兴奋。(我不得不发动突击队突袭,从鲍勃·马丁手中抢走了这本书,因为我想让它成为我的系列丛书。)像任何一本好的模式书一样,它为该领域的新人提供了知识,同样重要的是,它为经验丰富的从业者提供了词汇和基础,让他们将知识传授给他们的同事。对许多人来说,著名的四人帮书籍《设计模式》揭开了面向对象设计的隐藏宝石。这本书对 xUnit 也起到了同样的作用。

I've been excited by this project since I first heard about it. (I had to launch a commando raid to steal this book from Bob Martin because I wanted it to grace my series instead.) Like any good patterns book it provides knowledge to new people in the field, and just as important, provides the vocabulary and foundations for experienced practitioners to pass their knowledge on to their colleagues. For many people, the famous Gang of Four book Design Patterns unlocked the hidden gems of object-oriented design. This book does the same for xUnit.

Martin Fowler

系列编辑

ThoughtWorks 首席科学家

Martin Fowler

Series Editor

Chief Scientist, ThoughtWorks

前言

Preface

 

自我测试代码的价值

The Value of Self-Testing Code

《重构》 [Ref]第 4 章中,Martin Fowler 写道:

In Chapter 4 of Refactoring [Ref], Martin Fowler writes:

如果你看看大多数程序员是如何度过他们的时间的,你会发现编写代码实际上只是一小部分。有些时间花在弄清楚应该做什么,有些时间花在设计上,但大多数时间都花在调试上。我相信每个读者都能记得长时间的调试,经常到深夜。每个程序员都可以讲述一个花了一整天(或更长时间)才找到的错误的故事。修复错误通常很快,但找到它却是一场噩梦。然后当你修复了一个错误时,总是有可能出现另一个错误,而你可能直到很晚才注意到它。然后你花了很长时间才找到那个错误。

If you look at how most programmers spend their time, you'll find that writing code is actually a small fraction. Some time is spent figuring out what ought to be going on, some time is spent designing, but most time is spent debugging. I'm sure every reader can remember long hours of debugging, often long into the night. Every programmer can tell a story of a bug that took a whole day (or more) to find. Fixing the bug is usually pretty quick, but finding it is a nightmare. And then when you do fix a bug, there's always a chance that anther one will appear and that you might not even notice it until much later. Then you spend ages finding that bug.

 

有些软件很难手动测试。在这种情况下,我们常常被迫编写测试程序。

Some software is very difficult to test manually. In these cases, we are often forced into writing test programs.

我记得 1996 年我从事的一个项目。我的任务是构建一个事件框架,让客户端软件注册事件,并在其他软件引发该事件时收到通知(观察者[GOF]模式)。如果不编写一些示例客户端软件,我想不出测试这个框架的方法。我需要测试大约 20 种不同的场景,因此我为每个场景编写了所需数量的观察者、事件和事件引发器。起初,我记录了控制台中发生的事情并手动扫描。这种扫描很快就变得非常繁琐。

I recall a project I was working on in 1996. My task was to build an event framework that would let client software register for an event and be notified when some other software raised that event (the Observer [GOF] pattern). I could not think of a way to test this framework without writing some sample client software. I had about 20 different scenarios I needed to test, so I coded up each scenario with the requisite number of observers, events, and event raisers. At first, I logged what was occurring in the console and scanned it manually. This scanning became very tedious very quickly.

由于我很懒,我自然而然地寻找一种更简单的方法来执行此测试。对于每个测试,我都会填充一个Dictionary由预期事件和预期接收者索引的条目,并以接收者的名称作为值。当特定接收者收到事件通知时,它会在中查找Dictionary由其自身和刚刚收到的事件索引的条目。如果此条目存在,则接收者会删除该条目。如果不存在,则接收者会添加该条目,并显示一条错误消息,说明这是一个意外事件通知。

Being quite lazy, I naturally looked for an easier way to perform this testing. For each test I populated a Dictionary indexed by the expected event and the expected receiver of it with the name of the receiver as the value. When a particular receiver was notified of the event, it looked in the Dictionary for the entry indexed by itself and the event it had just received. If this entry existed, the receiver removed the entry. If it didn't, the receiver added the entry with an error message saying it was an unexpected event notification.

运行完所有测试后,测试程序只需查看Dictionary并打印出其中的内容(如果内容不为空)。因此,运行我的所有测试几乎没有任何成本。测试要么悄无声息地通过,要么给出一串测试失败的列表。出于必要性,我无意中发现了Mock 对象第 544页)和测试自动化框架第 298页)的概念!

After running all the tests, the test program merely looked in the Dictionary and printed out its contents if it was not empty. As a result, running all of my tests had a nearly zero cost. The tests either passed quietly or spewed a list of test failures. I had unwittingly discovered the concept of a Mock Object (page 544) and a Test Automation Framework (page 298) out of necessity!

我的第一个 XP 项目

My First XP Project

1999 年末,我参加了 OOPSLA 会议,在会上我拿到了一本 Kent Beck 的新书《极限编程详解》 [XPE]。我习惯于进行迭代和增量开发,并且已经相信自动化单元测试的价值,尽管我还没有尝试将其普遍应用。我非常尊重 Kent,我从 1994 年第一届 PLoP 1会议开始就认识他。出于所有这些原因,我决定尝试在 ClearStream Consulting 项目中应用极限编程。OOPSLA 会议结束后不久,我很幸运地遇到了一个适合尝试这种开发方法的项目 - 即一个与现有数据库交互但没有用户界面的附加应用程序。客户愿意以不同的方式开发软件。

In late 1999, I attended the OOPSLA conference, where I picked up a copy of Kent Beck's new book, eXtreme Programming Explained [XPE]. I was used to doing iterative and incremental development and already believed in the value of automated unit testing, although I had not tried to apply it universally. I had a lot of respect for Kent, whom I had known since the first PLoP 1 conference in 1994. For all these reasons, I decided that it was worth trying to apply eXtreme Programming on a ClearStream Consulting project. Shortly after OOPSLA, I was fortunate to come across a suitable project for trying out this development approach—namely, an add-on application that interacted with an existing database but had no user interface. The client was open to developing software in a different way.

我们开始“按书本”进行极限编程,使用了书中推荐的几乎所有实践,包括结对编程、集体所有权和测试驱动开发。当然,我们在弄清楚如何测试应用程序行为的某些方面时遇到了一些挑战,但我们仍然设法为大多数代码编写了测试。然后,随着项目的进展,我开始注意到一个令人不安的趋势:执行看似相似的任务所花的时间越来越长。

We started doing eXtreme Programming "by the book" using pretty much all of the practices it recommended, including pair programming, collective ownership, and test-driven development. Of course, we encountered a few challenges in figuring out how to test some aspects of the behavior of the application, but we still managed to write tests for most of the code. Then, as the project progressed, I started to notice a disturbing trend: It was taking longer and longer to implement seemingly similar tasks.

我向开发人员解释了这个问题,并要求他们在每张任务卡上记录编写新测试、修改现有测试和编写生产代码所花费的时间。很快,一个趋势就出现了。虽然编写新测试和编写生产代码所花费的时间似乎基本保持不变,但修改现有测试所花费的时间却在增加,因此开发人员的估算值也在上升。当一名开发人员要求我结对完成一项任务时,我们花了 90% 的时间来修改现有测试以适应一个相对较小的更改,我知道我们必须做出一些改变,而且要快!

I explained the problem to the developers and asked them to record on each task card how much time had been spent writing new tests, modifying existing tests, and writing the production code. Very quickly, a trend emerged. While the time spent writing new tests and writing the production code seemed to be staying more or less constant, the amount of time spent modifying existing tests was increasing and the developers' estimates were going up as a result. When a developer asked me to pair on a task and we spent 90% of the time modifying existing tests to accommodate a relatively minor change, I knew we had to change something, and soon!

当我们分析引入新功能时遇到的编译错误和测试失败类型时,我们发现许多测试都受到被测系统 (SUT) 方法变化的影响。当然,这并不奇怪。令人惊讶的是,大部分影响是在测试的装置设置部分感受到的,而且这些变化并没有影响测试的核心逻辑。

When we analyzed the kinds of compile errors and test failures we were experiencing as we introduced the new functionality, we discovered that many of the tests were affected by changes to methods of the system under test (SUT). This came as no surprise, of course. What was surprising was that most of the impact was felt during the fixture setup part of the test and that the changes were not affecting the core logic of the tests.

这一发现意义重大,因为它表明我们掌握了有关如何创建 SUT 对象的知识,而这些信息分散在大多数测试中。换句话说,测试对 SUT 行为的非必要部分了解太多。我说“非必要”是因为大多数受影响的测试并不关心夹具中的对象是如何创建的;它们关心确保这些对象处于正确的状态。经过进一步检查,我们发现许多测试在其测试夹具中创建了相同或几乎相同的对象。

This revelation was an important discovery because it showed us that we had the knowledge about how to create the objects of the SUT scattered across most of the tests. In other words, the tests knew too much about nonessential parts of the behavior of the SUT. I say "nonessential" because most of the affected tests did not care about how the objects in the fixture were created; they were interested in ensuring that those objects were in the correct state. Upon further examination, we found that many of the tests were creating identical or nearly identical objects in their test fixtures.

这个问题的明显解决方案是将这个逻辑分解成一小组测试实用程序方法第 599页)。有几种变体:

The obvious solution to this problem was to factor out this logic into a small set of Test Utility Methods (page 599). There were several variations:

  • 当我们有一堆需要相同对象的测试时,我们只需创建一种方法来返回可供使用的对象。我们现在将这些方法称为创建方法第 415页)。
  • When we had a bunch of tests that needed identical objects, we simply created a method that returned that kind of object ready to use. We now call these Creation Methods (page 415).
  • 有些测试需要为对象的某些属性指定不同的值。在这些情况下,我们将该属性作为参数传递给参数化创建方法(请参阅创建方法)。
  • Some tests needed to specify different values for some attribute of the object. In these cases, we passed that attribute as a parameter to the Parameterized Creation Method (see Creation Method).
  • 有些测试希望创建一个格式错误的对象,以确保 SUT 会拒绝它。为每个属性编写一个单独的参数化创建方法会使我们的测试助手(第643页) 的签名变得混乱,因此我们创建了一个有效的对象,然后替换了一个错误属性的值(请参阅第 718页的派生值 )。
  • Some tests wanted to create a malformed object to ensure that the SUT would reject it. Writing a separate Parameterized Creation Method for each attribute cluttered the signature of our Test Helper (page 643), so we created a valid object and then replaced the value of the One Bad Attribute (see Derived Value on page 718).

我们发现了我们的第一个测试自动化模式。

We had discovered what would become 2 our first test automation patterns.

后来,当测试开始失败时,因为数据库不喜欢我们试图插入另一个具有唯一约束的相同键的对象,我们添加了代码以编程方式生成唯一键。我们将此变体称为匿名创建方法(请参阅创建方法),以指示此添加行为的存在。

Later, when tests started failing because the database did not like the fact that we were trying to insert another object with the same key that had a unique constraint, we added code to generate the unique key programmatically. We called this variant an Anonymous Creation Method (see Creation Method) to indicate the presence of this added behavior.

识别出我们现在称之为“脆弱测试”第 239页)的问题是这个项目的重要事件,而随后对其解决方案模式的定义则使这个项目免于失败。如果没有这个发现,我们最好也只会放弃已经构建的自动化单元测试。最坏的情况是,这些测试会大大降低我们的生产率,以致我们无法履行对客户的承诺。事实证明,我们能够交付我们所承诺的产品,而且质量非常好。是的,测试人员3仍然在我们的代码中发现了错误,因为我们确实缺少一些测试。但是,一旦我们弄清楚了缺少的测试应该是什么样的,引入修复这些错误所需的更改就是一个相对简单的过程。

Identifying the problem that we now call a Fragile Test (page 239) was an important event on this project, and the subsequent definition of its solution patterns saved this project from possible failure. Without this discovery we would, at best, have abandoned the automated unit tests that we had already built. At worst, the tests would have reduced our productivity so much that we would have been unable to deliver on our commitments to the client. As it turned out, we were able to deliver what we had promised and with very good quality. Yes, the testers 3 still found bugs in our code because we were definitely missing some tests. Introducing the changes needed to fix those bugs, once we had figured out what the missing tests needed to look like, was a relatively straightforward process, however.

我们被迷住了。自动化单元测试和测试驱动开发确实有效,从那时起我们就一直在使用它们。

We were hooked. Automated unit testing and test-driven development really did work, and we have been using them consistently ever since.

在后续项目中应用这些实践和模式时,我们遇到了新的问题和挑战。在每种情况下,我们都“剥开洋葱”来找到根本原因并想出解决方法。随着这些技术的成熟,我们已将它们添加到我们的自动化单元测试技术库中。

As we applied the practices and patterns on subsequent projects, we have run into new problems and challenges. In each case, we have "peeled the onion" to find the root cause and come up with ways to address it. As these techniques have matured, we have added them to our repertoire of techniques for automated unit testing.

我们首先在 XP2001 上发表的一篇论文中描述了其中一些模式。在与那次会议及后续会议的其他参与者的讨论中,我们发现许多同行都在使用相同或类似的技术。这将我们的方法从“实践”提升为“模式”(在特定环境中对重复出现的问题的重复解决方案)。第一篇关于测试异味的论文[RTC]也在同一次会议上发表,该论文基于[Ref]中首次描述的代码异味概念。

We first described some of these patterns in a paper presented at XP2001. In discussions with other participants at that and subsequent conferences, we discovered that many of our peers were using the same or similar techniques. That elevated our methods from "practice" to "pattern" (a recurring solution to a recurring problem in a context). The first paper on test smells [RTC] was presented at the same conference, building on the concept of code smells first described in [Ref].

我的动机

My Motivation

我非常相信自动化单元测试的价值。在过去的二十年中,我大部分时间都没有使用过自动化单元测试,我知道,有了自动化单元测试,我的职业生涯会比没有自动化单元测试好得多。我相信 xUnit 框架及其支持的自动化测试是软件开发领域真正伟大的进步之一。当我看到公司试图采用自动化单元测试,但由于缺乏关键信息和技能而未能成功时,我感到非常沮丧。

I am a great believer in the value of automated unit testing. I practiced software development without it for the better part of two decades, and I know that my professional life is much better with it than without it. I believe that the xUnit framework and the automated tests it enables are among the truly great advances in software development. I find it very frustrating when I see companies trying to adopt automated unit testing but being unsuccessful because of a lack of key information and skills.

作为 ClearStream Consulting 的软件开发顾问,我见过很多项目。有时我会在项目早期被叫去帮助客户确保他们“做对了事情”。然而,更多的时候,我被叫去时事情已经偏离了轨道。结果,我看到了很多导致测试异味的“最糟糕的做法”。如果我很幸运,而且我被叫去得足够早,我可以帮助客户从错误中恢复过来。如果不是,客户可能会对 TDD 和自动化单元测试的工作方式不太满意,而且人们会说自动化单元测试是在浪费时间。

As a software development consultant with ClearStream Consulting, I see a lot of projects. Sometimes I am called in early on a project to help clients make sure they "do things right." More often than not, however, I am called in when things are already off the rails. As a result, I see a lot of "worst practices" that result in test smells. If I am lucky and I am called early enough, I can help the client recover from the mistakes. If not, the client will likely muddle through less than satisfied with how TDD and automated unit testing worked—and the word goes out that automated unit testing is a waste of time.

事后看来,如果在正确的时间掌握正确的知识,大多数这些错误和最佳实践都很容易避免。但是,如何才能获得这些知识而不自己犯错误呢?尽管听起来有点自私,但雇佣一个有知识的人是学习任何新实践或技术最省时的方法。根据 Gerry Weinberg 的“Raspberry Jam 法则” [SoC] 4,参加课程或阅读书籍是一种效率低得多(但成本较低)的替代方案。我希望通过写下这些错误并提出避免它们的方法,我可以为您的项目省去很多麻烦,无论它是完全敏捷的还是比过去更敏捷的——“Raspberry Jam 法则”除外。

In hindsight, most of these mistakes and best practices are easily avoidable given the right knowledge at the right time. But how do you obtain that knowledge without making the mistakes for yourself? At the risk of sounding self-serving, hiring someone who has the knowledge is the most time-efficient way of learning any new practice or technology. According to Gerry Weinberg's "Law of Raspberry Jam" [SoC], 4 taking a course or reading a book is a much less effective (though less expensive) alternative. I hope that by writing down a lot of these mistakes and suggesting ways to avoid them, I can save you a lot of grief on your project, whether it is fully agile or just more agile than it has been in the past—the "Law of Raspberry Jam" not withstanding.

本书适合哪些人阅读

Who This Book Is For

我写这本书主要是针对那些想要编写更好测试的软件开发人员(程序员、设计师和架构师),以及那些需要了解开发人员在做什么以及为什么需要对开发人员进行足够的宽容以便他们能够学会做得更好的经理和教练!本书的重点是使用 xUnit 自动化的开发人员测试和客户测试。此外,一些高级模式适用于使用 xUnit 以外的技术自动化的测试。Rick Mugridge 和 Ward Cunningham 写了一本关于 Fit [FitB]的优秀书籍,他们提倡许多相同的做法。

I have written this book primarily for software developers (programmers, designers, and architects) who want to write better tests and for the managers and coaches who need to understand what the developers are doing and why the developers need to be cut enough slack so they can learn to do it even better! The focus here is on developer tests and customer tests that are automated using xUnit. In addition, some of the higher-level patterns apply to tests that are automated using technologies other than xUnit. Rick Mugridge and Ward Cunningham have written an excellent book on Fit [FitB], and they advocate many of the same practices.

开发人员可能希望从头到尾阅读这本书,但他们应该专注于浏览参考章节,而不是逐字逐句地阅读。重点应该放在全面了解存在哪些模式以及它们如何工作。然后,开发人员可以在需要时返回到特定模式。每个模式的前几个元素(直到并包括“何时使用它”部分)应该提供此概述。

Developers will likely want to read the book from cover to cover, but they should focus on skimming the reference chapters rather than trying to read them word for word. The emphasis should be on getting an overall idea of which patterns exist and how they work. Developers can then return to a particular pattern when the need for it arises. The first few elements (up to and include the "When to Use It" section) of each pattern should provide this overview.

经理和教练可能更愿意专注于阅读第一部分叙述”,也许还有第二部分测试味道”。他们可能还需要阅读第 18 章测试策略模式”,因为这些是他们需要理解的决策,并在开发人员完成这些模式时为他们提供支持。经理至少应该阅读第 3 章测试自动化的目标”

Managers and coaches might prefer to focus on reading Part I, The Narratives, and perhaps Part II, The Test Smells. They might also need to read Chapter 18, Test Strategy Patterns, as these are decisions they need to understand and provide support to the developers as they work their way through these patterns. At a minimum, managers should read Chapter 3, Goals of Test Automation.

关于封面照片

About the Cover Photo

Martin Fowler 签名系列的每一本书的封面上都有一张桥梁图片。当 Martin Fowler 问我是否可以“将我的作品用于他的系列”时,我的一个想法是“我应该把哪座桥放在封面上?”我想到测试可以避免软件发生灾难性故障,以及这与桥梁的关系。我立刻想到了几座著名的桥梁故障,包括“Galloping Gertie”(塔科马海峡大桥)和温哥华的钢铁工人纪念大桥(以在施工期间因桥的一部分倒塌而丧生的钢铁工人命名)。

Every book in the Martin Fowler Signature Series features a picture of a bridge on the cover. One of the thoughts I had when Martin Fowler asked if he could "steal me for his series" was "Which bridge should I put on the cover?" I thought about the ability of testing to avoid catastrophic failures of software and how that related to bridges. Several famous bridge failures immediately came to mind, including "Galloping Gertie" (the Tacoma Narrows bridge) and the Iron Workers Memorial Bridge in Vancouver (named for the iron workers who died when a part of it collapsed during construction).

经过进一步思考,声称测试可以避免这些失败似乎不太正确,因此我选择了一座与个人关系更密切的桥梁。封面上的图片是西弗吉尼亚州的新河峡谷大桥。20 世纪 80 年代末,我在一次激流皮划艇之旅中第一次经过这座桥,随后划过这座桥。这座桥的风格也与本书的内容相关:桥下复杂的拱形结构对于使用它到达峡谷另一侧的人来说基本上是隐藏的。路面完全平坦,有四条车道宽,通行非常顺畅。事实上,在晚上,人们完全可能没有意识到自己身处谷底数千英尺的高空。良好的测试自动化基础设施具有相同的效果:编写测试很容易,因为大多数复杂性都隐藏在路基之下。

After further reflection, it just did not seem right to claim that testing might have prevented these failures, so I chose a bridge with a more personal connection. The picture on the cover shows the New River Gorge bridge in West Virginia. I first passed over and subsequently paddled under this bridge on a whitewater kayaking trip in the late 1980s. The style of the bridge is also relevant to this book's content: The complex arch structure underneath the bridge is largely hidden from those who use it to get to the other side of the gorge. The road deck is completely level and four lanes wide, resulting in a very smooth passage. In fact, at night it is quite possible to remain completely oblivious to the fact that one is thousands of feet above the valley floor. A good test automation infrastructure has the same effect: Writing tests is easy because most of the complexity lies hidden beneath the road bed.

版权页

Colophon

本书的手稿是用 XML 编写的,我将其发布为 HTML 以便在我的网站上预览。我使用 Eclipse 和 XML Buddy 插件编辑 XML。HTML 是使用 Ruby 程序生成的,该程序最初是从 Martin Fowler 那里获得的,后来随着我开发自定义标记语言,我对它进行了广泛的改进。代码示例是在(大部分)Eclipse 中编写、编译和执行的,并由 XML 标记处理程序自动插入到 HTML 中(使用 Ruby 而不是 XSLT 的主要原因之一)。这使我能够“尽早发布,经常发布”到网站。我还可以从源代码为审阅者生成单个 Word 或 PDF 文档,尽管这需要一些手动步骤。

This book's manuscript was written using XML, which I published to HTML for previewing on my Web site. I edited the XML using Eclipse and the XML Buddy plug-in. The HTML was generated using a Ruby program that I first obtained from Martin Fowler and which I then evolved quite extensively as I evolved my custom markup language. Code samples were written, compiled, and executed in (mostly) Eclipse and were inserted into the HTML automatically by XML tag handlers (one of the main reasons for using Ruby instead of XSLT). This gave me the ability to "publish early, publish often" to the Web site. I could also generate a single Word or PDF document for reviewers from the source, although this required some manual steps.

致谢

Acknowledgments

 

虽然这本书主要是我一个人写的,但也有许多人以自己的方式为它做出了贡献。我提前向那些我可能漏掉的人致歉。

While this book is largely a solo writing effort, many people have contributed to it in their own ways. Apologies in advance to anyone whom I may have missed.

了解我的人可能会好奇我是如何找到足够的时间来写这样一本书的。当我不工作时,我通常会去做各种(有些人会说是“极限”)户外运动,比如越野(极限)滑雪、激流(极限)皮划艇和山地(极限)自行车。就我个人而言,我不同意用“极限”这个词来形容我的活动,就像我不同意用这个词来形容高度迭代和增量(极限)编程一样。然而,我哪里有时间写这本书的问题是一个合理的问题。我必须特别感谢我的朋友 Heather Armitage,我和她一起参与了上述大部分活动。她​​开车往返这些冒险之旅,路上我弯腰坐在副驾驶座上,对着笔记本电脑写这本书,路上她开了很多个小时的车。另外,还要感谢 Alf Skrastins,他喜欢开着他的 Previa 载着他所有的朋友去卡尔加里以西的越野滑雪场。另外,还要感谢各个越野滑雪旅馆的经营者,他们允许我用他们的发电机为我的笔记本电脑充电,这样我就可以在度假期间写这本书了——Selkirk Lodge 的 Grania Devine、Sorcerer Lodge 的 Tannis Dakin 以及 Sol Mountain Touring 的 Dave Flear 和 Aaron Cooperman。如果没有他们的帮助,这本书的写作时间会更长!

People who know me well may wonder how I found enough time to write a book like this. When I am not working, I am usually off doing various (some would say "extreme") outdoor sports, such as back-country (extreme) skiing, whitewater (extreme) kayaking, and mountain (extreme) biking. Personally, I do not agree with this application of the "extreme" adjective to my activities any more than I agree with its use for highly iterative and incremental (extreme) programming. Nevertheless, the question of where I found the time to write this book is a valid one. I must give special thanks to my friend Heather Armitage, with whom I engage in most of the above activities. She has driven many long hours on the way to or from these adventures with me hunched over my laptop computer in the passenger seat working on this book. Also, thanks go to Alf Skrastins, who loves to drive all his friends to back-country skiing venues west of Calgary in his Previa. Also, thanks to the operators of the various back-country ski lodges who let me recharge my laptop from their generators so I could work on the book while on vacation—Grania Devine at Selkirk Lodge, Tannis Dakin at Sorcerer Lodge, and Dave Flear and Aaron Cooperman at Sol Mountain Touring. Without their help, this book would have taken much longer to write!

和往常一样,我要感谢我的所有审阅者,包括官方和非官方的。罗伯特·C·马丁 (“鲍勃叔叔”) 审阅了初稿。第一稿“官方”的官方审阅者是 Lisa Crispin 和 Rick Mugridge。Lisa Crispin、Jeremy Miller、Alistair Duguid、Michael Hedgpeth 和 Andrew Stopford 审阅了第二稿。

As usual, I'd like to thank all my reviewers, both official and unofficial. Robert C. ("Uncle Bob") Martin reviewed an early draft. The official reviewers of the first "official" draft were Lisa Crispin and Rick Mugridge. Lisa Crispin, Jeremy Miller, Alistair Duguid, Michael Hedgpeth, and Andrew Stopford reviewed the second draft.

感谢来自各个 PLoP 会议的“指导者”,他们为这些模式的草稿提供了反馈意见——Michael Stahl、Danny Dig,尤其是 Joe Yoder;他们对我对模式形式的实验提供了专家意见。我还要感谢 PLoP 2004 模式语言 PLoP 研讨会小组的成员,尤其是 Eugene Wallingford、Ralph Johnson 和 Joseph Bergin。Brian Foote 和 UIUC 的 SAG 小组发布了几 GB 的 MP3,记录了他们讨论本书早期草稿的评审会议。他们的评论促使我从头开始重写了至少一个叙述章节。

Thanks to my "shepherds" from the various PLoP conferences who provided feedback on drafts of these patterns—Michael Stahl, Danny Dig, and especially Joe Yoder; they provided expert comments on my experiments with the pattern form. I would also like to thank the members of the PLoP workshop group on Pattern Languages at PLoP 2004 and especially Eugene Wallingford, Ralph Johnson, and Joseph Bergin. Brian Foote and the SAG group at UIUC posted several gigabytes of MP3's of the review sessions in which they discussed the early drafts of the book. Their comments caused me to rewrite from scratch at least one of the narrative chapters.

许多人通过电子邮件向我发送了有关我在网站http://xunitpatterns.com上发布的材料的评论,或者在 Yahoo! 组上发表评论;他们对我在那里发布的有时非常草稿的材料提供了非常及时的反馈。这些人包括 Javid Jamae、Philip Nelson、Tomasz Gajewski、John Hurst、Sven Gorts、Bradley T. Landis、Cédric Beust、Joseph Pelrine、Sebastian Bergmann、Kevin Rutherford、Scott W. Ambler、JB Rainsberger、Oli Bye、Dale Emery、David Nunn、Alex Chaffee、Burkhardt Hufnagel、Johannes Brodwall、Bret Pettichord、Clint Shank、Sunil Joglekar、Rachel Davies、Nat Pryce、Paul Hodgetts、Owen Rogers、Amir Kolsky、Kevin Lawrence、Alistair Cockburn、Michael Feathers 和 Joe Schmetzer。特别感谢 Neal Norwitz、Markus Gaelli、Stephane Ducasse 和 Stefan Reichhart,他们作为非官方审阅者提供了大量反馈。

Many people e-mailed me comments about the material posted on my Web site at http://xunitpatterns.com or posted comments on the Yahoo! group; they provided very timely feedback on the sometimes very draft-like material I had posted there. These folks included Javid Jamae, Philip Nelson, Tomasz Gajewski, John Hurst, Sven Gorts, Bradley T. Landis, Cédric Beust, Joseph Pelrine, Sebastian Bergmann, Kevin Rutherford, Scott W. Ambler, J. B. Rainsberger, Oli Bye, Dale Emery, David Nunn, Alex Chaffee, Burkhardt Hufnagel, Johannes Brodwall, Bret Pettichord, Clint Shank, Sunil Joglekar, Rachel Davies, Nat Pryce, Paul Hodgetts, Owen Rogers, Amir Kolsky, Kevin Lawrence, Alistair Cockburn, Michael Feathers, and Joe Schmetzer. Special thanks go to Neal Norwitz, Markus Gaelli, Stephane Ducasse, and Stefan Reichhart, who provided copious feedback as unofficial reviewers.

不少人给我发电子邮件,描述他们最喜欢的模式或 xUnit 系列成员的特殊功能。其中大部分都是我已经记录的模式的变体;我已将它们作为别名或实现变体包含在本书中(视情况而定)。有些是比较深奥的模式,由于篇幅原因我不得不省略 — 对此,我深表歉意。

Quite a few people sent me e-mails describing their favorite pattern or special feature from their member of the xUnit family. Most of these were variations on patterns I had already documented; I've included them in this book as aliases or implementation variations as appropriate. A few were more esoteric patterns that I had to leave out for space reasons—for that, I apologize.

本书中描述的许多想法都来自我与 ClearStream Consulting 的同事共同开展的项目。在极限编程的早期,我们互相鼓励,寻找更好的方法,当时可用的资源很少(如果有的话)。正是这种一心一意的决心导致了这里描述的许多更有用的技术。这些同事是 Jennitta Andrea、Ralph Bohnet、Dave Braat、Russel Bryant、Greg Cook、Geoff Hardy、Shaun Smith 和 Thomas (T2) Tannahill。他们中的许多人还提供了各个章节的早期评论。Greg 还提供了第 25 章“数据库模式”中的许多代码示例,而 Ralph 为我的网站设置了 CVS 存储库和自动构建过程。我还要感谢 ClearStream 的老板,他们让我从咨询工作中抽出时间来编写这本书,并允许我将为期两天的“开发人员测试”课程中的基于代码的练习作为许多代码示例的基础。谢谢,Denis Clelland 和 Luke McFarlane!

Many of the ideas described in this book came from projects I worked on with my colleagues from ClearStream Consulting. We all pushed one another to find better ways of doing things back in the early days of eXtreme Programming when few—if any—resources were available. It was this single-minded determination that led to many of the more useful techniques described here. Those colleagues are Jennitta Andrea, Ralph Bohnet, Dave Braat, Russel Bryant, Greg Cook, Geoff Hardy, Shaun Smith, and Thomas (T2) Tannahill. Many of them also provided early reviews of various chapters. Greg also provided many of the code samples in Chapter 25, Database Patterns, while Ralph set up my CVS repository and automated build process for the Web site. I would also like to thank my bosses at ClearStream, who let me take time off from consulting engagements to work on the book and for permission to use the code-based exercises from our two-day "Testing for Developers" course as the basis for many of the code samples. Thanks, Denis Clelland and Luke McFarlane!

当事情变得艰难时,有几个人鼓励我继续写这本书。他们总是愿意接听电话,讨论我遇到的一些棘手问题。其中最突出的是 Joshua Kerievsky 和 ​​Martin Fowler。

Several people encouraged me to keep working on the book when the going got tough. They were always willing to take a phone call to discuss some sticky issue I was grappling with. Foremost among these individuals were Joshua Kerievsky and Martin Fowler.

我要特别感谢 Shaun Smith 帮助我开始写这本书,并在写作初期提供技术支持。他托管了我的网站,创建了第一个 CSS 样式表,教我 Ruby,建立了一个 wiki 来讨论模式,甚至在个人和工作需求迫使他退出项目的写作方面之前提供了一些早期内容。每当我谈到经历时说“我们”,我可能指的是 Shaun 和我自己,尽管其他同事也可能有同样的看法。

I'd like to especially thank Shaun Smith for helping me get started on this book and for the technical support he provided throughout the early part of writing it. He hosted my Web site, created the first CSS style sheets, taught me Ruby, set up a wiki for discussing the patterns, and even provided some of the early content before personal and work demands forced him to pull out of the writing side of the project. Whenever I say "we" when I talk about experiences, I am probably referring to Shaun and myself, although other coworkers may also share the same opinion.

介绍

Introduction

 

之前已经说过,但值得重复:编写无缺陷的软件极其困难。证明真实系统的正确性仍然远远超出我们的能力,行为规范也同样具有挑战性。预测未来的需求是一件碰运气的事情——如果我们擅长预测,我们都会在股市上致富,而不是构建软件系统!

It has been said before but it bears repeating: Writing defect-free software is exceedingly difficult. Proof of correctness of real systems is still well beyond our abilities, and specification of behavior is equally challenging. Predicting future needs is a hit or miss affair—we'd all be getting rich on the stock market instead of building software systems if we were any good at it!

软件行为的自动验证是过去几十年来开发方法的最大进步之一。这种对开发人员非常友好的做法在提高生产力、提高质量和防止软件变得脆弱方面具有巨大的好处。现在有这么多开发人员自愿这样做,这一事实本身就说明了它的有效性。

Automated verification of software behavior is one of the biggest advances in development methods in the last few decades. This very developer-friendly practice has huge benefits in terms of increasing productivity, improving quality, and keeping software from becoming brittle. The very fact that so many developers are now doing it of their own free will speaks for its effectiveness.

本章介绍了使用各种工具(包括xUnit)进行测试自动化的概念,描述了为什么要这样做,并解释了哪些因素使得测试自动化变得难以做好。

This chapter introduces the concept of test automation using a variety of tools (including xUnit), describes why you would do it, and explains what makes it difficult to do test automation well.

反馈

Feedback

反馈是许多活动中非常重要的元素。反馈告诉我们我们的行动是否产生了正确的效果。我们越早得到反馈,我们就能越快地做出反应。这种反馈的一个很好的例子是现在许多高速公路上在主要行驶路面和路肩之间铺设的减速带。是的,驶离路肩会给我们反馈,告诉我们我们已经偏离了道路。但更早得到反馈(当我们的车轮第一次进入路肩时)会给我们更多的时间来纠正我们的路线,并降低我们驶离道路的可能性。

Feedback is a very important element in many activities. Feedback tells us whether our actions are having the right effect. The sooner we get feedback, the more quickly we can react. A good example of this kind of feedback is the rumble strips now being ground into many highways between the main driving surface and the shoulders. Yes, driving off the shoulder gives us feedback that we have left the road. But getting feedback earlier (when our wheels first enter the shoulder) gives us more time to correct our course and reduces the likelihood that we will drive off the road at all.

测试就是获取软件的反馈。因此,反馈是“敏捷”或“精益”软件开发的基本要素之一。在开发过程中拥有反馈循环可以让我们对自己编写的软件充满信心。它让我们可以更快地工作,减少疑虑。通过让测试告诉我们何时破坏了旧功能,它让我们专注于正在添加的新功能。

Testing is all about getting feedback on software. For this reason, feedback is one of the essential elements of "agile" or "lean" software development. Having feedback loops in the development process is what gives us confidence in the software that we write. It lets us work more quickly and with less paranoia. It lets us focus on the new functionality we are adding by having the tests tell us whenever we break old functionality.

测试

Testing

“测试”的传统定义来自质量保证领域。我们测试软件是因为我们确信它有缺陷!所以我们测试、测试、再测试,直到我们无法证明软件中仍然有缺陷。传统上,这种测试是在软件完成后进行的。因此,它是一种衡量质量的方法,而不是一种将质量融入产品的方法。在许多组织中,测试是由软件开发人员以外的人进行的。这种测试提供的反馈非常有价值,但它在开发周期中出现得太晚了,因此其价值大大降低了。它还具有延长时间表的不良影响,因为发现的问题会被送回开发部门进行返工,然后再进行另一轮测试。那么软件开发人员应该进行哪种测试才能更早地获得反馈呢?

The traditional definition of "testing" comes from the world of quality assurance. We test software because we are sure it has bugs in it! So we test and we test and we test some more, until we cannot prove there are still bugs in the software. Traditionally, this testing occurs after the software is complete. As a result, it is a way of measuring quality—not a way of building quality into the product. In many organizations, testing is done by someone other than the software developers. The feedback provided by this kind of testing is very valuable, but it comes so late in the development cycle that its value is greatly diminished. It also has the nasty effect of extending the schedule as the problems found are sent back to development for rework, to be followed by another round of testing. So what kind of testing should software developers do to get feedback earlier?

开发人员测试

Developer Testing

很少有软件开发人员相信自己能够编写出“第一次就能成功”的代码。事实上,当某件事第一次就能成功时,我们大多数人都会感到惊喜。(我希望我没有打破非开发人员读者的幻想!)

Rare is the software developer who believes he or she can write code that works "first time, every time." In fact, most of us are pleasantly surprised when something does work the first time. (I hope I am not shattering any illusions for the nondeveloper readers out there!)

因此,开发人员也会进行测试。我们希望向自己证明软件能够按照我们的预期运行。一些开发人员可能会采用与测试人员相同的方式进行测试:将整个系统作为一个整体进行测试。然而,大多数开发人员更喜欢逐个单元地测试他们的软件。这些“单元”可能是粒度更大的组件,也可能是单独的类、方法或函数。这些测试与测试人员编写的测试之间的关键区别在于,被测试的单元是软件设计的结果,而不是需求的直接体现。1

So developers do testing, too. We want to prove to ourselves that the software works as we intended it to. Some developers might do their testing the same way as testers do it: by testing the whole system as a single entity. Most developers, however, prefer to test their software unit by unit. The "units" may be larger-grained components or they may be individual classes, methods, or functions. The key thing that distinguishes these tests from the ones that the testers write is that the units being tested are a consequence of the design of the software, rather than being a direct translation of the requirements.1

自动化测试

Automated Testing

自动化测试已经存在了几十年。20 世纪 80 年代初,当我在 Nortel 的研发子公司 Bell-Northern Research 从事电话交换系统工作时,我们对正在构建的软件/硬件进行了自动回归和负载测试。这种测试主要在“系统测试”组织的背景下进行,使用编写了测试脚本的专用硬件和软件。测试机器连接到被测试的交换机,就好像它是一堆电话和其他电话交换机一样;它拨打电话并执行各种电话功能。当然,这种自动化测试基础设施不适合单元测试,而且由于涉及大量硬件,开发人员通常也无法使用它。

Automated testing has been around for several decades. When I worked on telephone switching systems at Nortel's R&D subsidiary Bell-Northern Research in the early 1980s, we did automated regression and load testing of the software/hardware that we were building. This testing was done primarily in the context of the "System Test" organization using specialized hardware and software that were programmed with test scripts. The test machines connected to the switch being tested as though it were a bunch of telephones and other telephone switches; it made telephone calls and exercised the myriad of telephone features. Of course, this automated testing infrastructure was not suitable for unit testing, nor was it generally available to the developers because of the huge amounts of hardware involved.

在过去十年中,出现了更多通用的测试自动化工具,可通过用户界面测试应用程序。其中一些工具使用脚本语言来定义测试;更吸引人的工具则依靠“机器人用户”或“录制和回放”隐喻来实现测试自动化。不幸的是,这些工具的许多早期体验都让测试人员和测试经理不太满意。原因是“脆弱测试”问题导致测试维护成本高昂。

In the last decade, more general-purpose test automation tools have become available for testing applications through their user interfaces. Some of these tools use scripting languages to define the tests; the sexier tools rely on the "robot user" or "record and playback" metaphor for test automation. Unfortunately, many of the early experiences with these latter tools left the testers and test managers less than satisfied. The cause was high test maintenance costs caused by the "fragile test" problem.

“脆弱测试”问题

The "Fragile Test" Problem

使用商业“录制和回放”或“机器人用户”工具的测试自动化在这些工具的早期用户中名声不佳。使用这种方法自动化的测试经常会因为看似微不足道的原因而失败。了解这种测试自动化风格的局限性很重要,以避免陷入通常与之相关的陷阱 - 即行为敏感性、界面敏感性、数据敏感性和上下文敏感性。

Test automation using commercial "record and playback" or "robot user" tools has gained a bad reputation among early users of these tools. Tests automated using this approach often fail for seemingly trivial reasons. It is important to understand the limitations of this style of test automation to avoid falling victim to the pitfalls commonly associated with it—namely, behavior sensitivity, interface sensitivity, data sensitivity, and context sensitivity.

行为敏感性

如果系统的行为发生变化(例如,如果需求发生变化,系统需要修改以满足新需求),那么任何执行修改后功能的测试在重放时很可能会失败。2无论使用哪种测试自动化方法,这都是测试的基本现实。真正的问题是,我们经常需要使用该功能将系统调整到正确的状态以开始测试。因此,行为变化对测试过程的影响比人们预期的要大得多。

If the behavior of the system is changed (e.g., if the requirements are changed and the system is modified to meet the new requirements), any tests that exercise the modified functionality will most likely fail when replayed.2 This is a basic reality of testing regardless of the test automation approach used. The real problem is that we often need to use that functionality to maneuver the system into the right state to start a test. As a consequence, behavioral changes have a much larger impact on the testing process than one might expect.

界面灵敏度

通过用户界面测试被测系统 (SUT)内部的业务逻辑不是一个好主意。即使对界面进行微小的更改也可能导致测试失败,尽管人类用户可能会说测试仍应通过。这种意想不到的界面敏感性是过去十年中测试自动化工具声名狼藉的原因之一。尽管无论使用哪种用户界面技术都会发生这个问题,但某些类型的界面似乎比其他界面更严重。图形用户界面 (GUI) 是一种与系统内部业务逻辑交互的特别具有挑战性的方式。最近向基于 Web (HTML) 的用户界面的转变使测试自动化的某些方面变得更容易,但由于 HTML 中需要可执行代码来提供丰富的用户体验,因此又引入了另一个问题。

Testing the business logic inside the system under test (SUT) via the user interface is a bad idea. Even minor changes to the interface can cause tests to fail, even though a human user might say the test should still pass. Such unintended interface sensitivity is partly what gave test automation tools such a bad name in the past decade. Although the problem occurs regardless of which user interface technology is being used, it seems to be worse with some types of interfaces than with others. Graphical user interfaces (GUIs) are a particularly challenging way to interact with the business logic inside the system. The recent shift to Web-based (HTML) user interfaces has made some aspects of test automation easier but has introduced yet another problem because of the executable code needed within the HTML to provide a rich user experience.

数据敏感性

所有测试都假设某个起点,称为测试装置;这个测试环境有时被称为测试的“前提条件”或“之前的图片”。最常见的是,这个测试装置是根据系统中已有的数据定义的。如果数据发生变化,测试可能会失败,除非付出巨大努力使测试对所使用的数据不敏感。

All tests assume some starting point, called the test fixture; this test context is sometimes called the "pre-conditions" or "before picture" of the test. Most commonly, this test fixture is defined in terms of data that is already in the system. If the data changes, the tests may fail unless great effort has been expended to make them insensitive to the data being used.

上下文敏感性

系统的行为可能会受到系统外部事物状态的影响。这些外部因素可能包括设备(例如打印机、服务器)的状态、其他应用程序的状态,甚至系统时钟(例如测试执行的时间和/或日期)。如果不控制上下文,任何受此上下文影响的测试都将难以确定性地重复。

The behavior of the system may be affected by the state of things outside the system. These external factors could include the states of devices (e.g., printers, servers), other applications, or even the system clock (e.g., the time and/or date of execution of the test). Any tests that are affected by this context will be difficult to repeat deterministically without getting control over the context.

克服四种敏感

Overcoming the Four Sensitivities

无论我们使用哪种技术来自动化测试,这四种敏感性都是存在的。当然,有些技术为我们提供了解决这些敏感性的方法,而其他技术则迫使我们走上特定的道路。xUnit 系列测试自动化框架为我们提供了很大程度的控制权;我们只需学习如何有效地使用它。

The four sensitivities exist regardless of which technology we use to automate the tests. Of course, some technologies give us ways to work around these sensitivities, while others force us down a particular path. The xUnit family of test automation frameworks gives us a large degree of control; we just have to learn how to use it effectively.

自动化测试的用途

Uses of Automated Tests

到目前为止,这里的大部分讨论都集中在应用程序的回归测试上。这是修改现有应用程序时非常有价值的反馈形式,因为它可以帮助我们发现无意中引入的缺陷。

Thus far, most of the discussion here has centered on regression testing of applications. This is a very valuable form of feedback when modifying existing applications because it helps us catch defects that we have introduced inadvertently.

测试作为规范

Tests as Specification

自动化测试的另一种完全不同的用途体现在测试驱动开发( TDD ) 中,这是极限编程等敏捷方法的核心实践之一。自动化测试的这种用途更多地是规范尚未编写的软件的行为,而不是回归测试。TDD 的有效性在于它让我们将对软件的思考分为两个独立的阶段:它应该做什么,以及它应该如何做。

A completely different use of automated testing is seen in test-driven development (TDD), which is one of the core practices of agile methods such as eXtreme Programming. This use of automated testing is more about specification of the behavior of the software yet to be written than it is about regression testing. The effectiveness of TDD comes from the way it lets us separate our thinking about software into two separate phases: what it should do, and how it should do it.

等一下!敏捷软件开发的支持者不是避开瀑布式开发吗?是的,确实如此。敏捷主义者更喜欢逐个功能地设计和构建系统,在他们继续开发下一个功能之前,每一步都有可用的工作软件来证明每个功能都有效。这并不意味着我们不做设计;它只是意味着我们做“连续设计”!将这一点发挥到极致会导致“紧急设计”,其中很少进行预先设计。但开发不必以这种方式进行。我们可以将高级设计(或架构)与逐个功能的详细设计相结合。无论哪种方式,在我们以可执行规范的形式捕获该行为应该是什么时,将思考如何实现特定类或方法的行为延迟几分钟是有用的。毕竟,我们大多数人都难以一次专注于一件事,更不用说同时做几件事了。

Hold on a minute! Don't the proponents of agile software development eschew waterfall-style development? Yes, indeed. Agilists prefer to design and build a system feature by feature, with working software being available at every step to prove that each feature works before they move on to develop the next feature. That does not mean we do not do design; it simply means we do "continuous design"! Taking this to the extreme results in "emergent design," where very little design is done upfront. But development does not have to be done that way. We can combine high-level design (or architecture) upfront with detailed design on a feature-by-feature basis. Either way, it can be useful to delay thinking about how to achieve the behavior of a specific class or method for a few minutes while we capture what that behavior should be in the form of an executable specification. After all, most of us have trouble concentrating on one thing at a time, let alone several things simultaneously.

一旦我们完成测试的编写并验证它们是否按预期失败,我们就可以转换视角,专注于让它们通过。测试现在充当进度测量。如果我们逐步实现功能,我们可以看到随着我们编写更多代码,每个测试都会逐一通过。在工作过程中,我们会继续运行所有之前编写的测试作为回归测试,以确保我们的更改没有任何意外的副作用。这就是自动化单元测试的真正价值所在:它能够“确定” SUT 的功能,以便不会意外更改功能。这就是让我们晚上睡得安稳的原因!

Once we have finished writing the tests and verifying that they fail as expected, we can switch our perspective and focus on making them pass. The tests are now acting as a progress measurement. If we implement the functionality incrementally, we can see each test pass one by one as we write more code. As we work, we keep running all of the previously written tests as regression tests to make sure our changes have not had any unexpected side effects. This is where the true value of automated unit testing lies: in its ability to "pin down" the functionality of the SUT so that the functionality is not changed accidentally. That is what allows us to sleep well at night!

测试驱动开发

最近有很多关于测试驱动开发主题的书,所以这本书不会花太多篇幅来讨论这个主题。这本书关注的是测试中的代码是什么样子的,而不是我们什么时候编写测试的。当我们研究测试的重构并学习如何将使用一种模式编写的测试重构为使用具有不同特征的模式的测试时,我们将最接近讨论测试是如何产生的。

Many books have been written recently on the topic of test-driven development, so this one will not devote a lot of space to that topic. This book focuses on what the code in the tests looks like, rather than when we wrote the tests. The closest we will get to talking about how the tests come into being is when we investigate refactoring of tests and learn how to refactor tests written using one pattern into tests that use a pattern with different characteristics.

我在本书中试图保持“开发过程不可知论”的立场,因为自动化测试可以帮助任何团队,无论其成员是进行 TDD、测试先行开发还是测试后开发。此外,一旦人们学会了如何在“测试后”环境中实现自动化测试,他们就可能更倾向于尝试“测试先行”方法。尽管如此,我们确实探索了开发过程的某些部分,因为它们会影响我们实现测试自动化的难易程度。这项调查有两个关键方面:(1)全自动测试(见第26页)与我们的开发集成过程和工具之间的相互作用,以及 (2) 开发过程如何影响我们设计的可测试性。

I am trying to stay "development process agnostic" in this book because automated testing can help any team regardless of whether its members are doing TDD, test-first development, or test-last development. Also, once people learn how to automate tests in a "test last" environment, they are likely to be more inclined to experiment with a "test first" approach. Nevertheless, we do explore some parts of the development process because they affect how easily we can do test automation. There are two key aspects of this investigation: (1) the interplay between Fully Automated Tests (see page 26) and our development integration process and tools, and (2) the way in which the development process affects the testability of our designs.

模式

Patterns

在准备撰写本书时,我阅读了大量关于基于 xUnit 的测试自动化的会议论文和书籍。毫不奇怪,每位作者似乎都有特定的关注领域和最喜欢的技术。虽然我并不总是同意他们的做法,但我总是试图理解为什么这些作者以特定的方式做事,以及何时使用他们的技术比我已经使用的技术更合适。

In preparing to write this book, I read a lot of conference papers and books on xUnit-based test automation. Not surprisingly, each author seems to have a particular area of interest and favorite techniques. While I do not always agree with their practices, I am always trying to understand why these authors do things a particular way and when it would be more appropriate to use their techniques than the ones I already use.

这种理解水平是示例与仅解释技术和模式“如何做”的散文之间的主要区别之一。模式可以帮助读者理解实践背后的原因,使他们能够在备选模式之间做出明智的选择,从而避免将来出现任何意想不到的严重后果。

This level of understanding is one of the major differences between examples and prose that merely explain the "how to" of a technique and a pattern. A pattern helps readers understand the why behind the practice, allowing them to make intelligent choices between the alternative patterns and thereby avoid any unexpected nasty consequences in the future.

软件模式已经存在了十年,所以大多数读者至少应该知道这个概念。模式是“重复出现的问题的解决方案”。有些问题比其他问题更大,因此无法用单一模式解决。这就是模式语言发挥作用的地方;这个模式集合(或语法)引导读者从总体问题逐步走向详细的解决方案。在模式语言中,一些模式必然具有更高的抽象级别,而其他模式则侧重于较低级别的细节。为了有用,模式之间必须有联系,以便我们可以从更高级别的“策略”模式逐步深入到更详细的“设计模式”和最详细的“编码习语”。

Software patterns have been around for a decade, so most readers should at least be aware of the concept. A pattern is a "solution to a recurring problem." Some problems are bigger than others and, therefore, too big to solve with a single pattern. That is where the pattern language comes into play; this collection (or grammar) of patterns leads the reader from an overall problem step by step to a detailed solution. In a pattern language, some of the patterns will necessarily be of higher levels of abstraction, while others will focus on lower-level details. To be useful, there must be linkages between the patterns so that we can work our way down from the higher-level "strategy" patterns to the more detailed "design patterns" and the most detailed "coding idioms."

模式、原则和气味

Patterns versus Principles versus Smells

本书包括三种模式。最传统的模式是“常见问题的重复解决方案”;本书中的大多数模式都属于这一大类。我确实区分了三个不同的级别:

This book includes three kinds of patterns. The most traditional kind of pattern is the "recurring solution to a common problem"; most of the patterns in this book fall into this general category. I do distinguish between three different levels:

  • “策略”级模式具有深远的影响。决定使用共享装置(第317页)而不是新鲜装置(第311页)将我们引向一条截然不同的道路,并导致一组不同的测试设计模式。每种策略模式在本书参考部分的“策略模式”一章中都有自己的描述。
  • "Strategy"-level patterns have far-reaching consequences. The decision to use a Shared Fixture (page 317) rather than a Fresh Fixture (page 311) takes us down a very different path and leads to a different set of test design patterns. Each of the strategy patterns has its own write-up in the "Strategy Patterns" chapter in the reference section of the book.
  • 测试“设计”级模式用于开发特定功能的测试。它们专注于我们如何组织测试逻辑。大多数读者应该熟悉的一个例子是Mock Object模式(第544页)。每个测试设计模式都有自己的说明,并且这些模式根据测试替身模式等主题在本书的参考部分中分组为章节。
  • Test "design"-level patterns are used when developing tests for specific functionality. They focus on how we organize our test logic. An example that should be familiar to most readers is the Mock Object pattern (page 544). Each test design pattern has its own write-up and the patterns are grouped into chapters in the reference section of the book based on topics such as Test Double patterns.
  • 测试“编码习语”描述了对特定测试进行编码的不同方法。其中许多是特定于语言的;示例包括在 Smalltalk 中使用块闭包进行预期异常测试(请参阅第348页的测试方法)以及在 Java 中使用匿名内部类进行模拟对象。有些,例如简单成功测试(请参阅测试方法),相当通用,因为它们在每种语言中都有类似物。这些习语通常列为“测试设计模式”编写中的实现变体或示例。
  • Test "coding idioms" describe different ways to code a specific test. Many of these are language specific; examples include using block closures for Expected Exception Tests (see Test Method on page 348) in Smalltalk and anonymous inner classes for Mock Objects in Java. Some, such as Simple Success Test (see Test Method), are fairly generic in that they have analogs in each language. These idioms are typically listed as implementation variations or examples within the write-up of a "test design pattern."

通常,每个级别都可以使用几种替代模式。当然,我几乎总是偏爱使用哪种模式,但一个人的“反模式”可能是另一个人的“最佳实践模式”。因此,本书包含我不一定提倡的模式。它描述了每种模式的优点和缺点,让读者能够就其使用做出明智的决定。我试图在每个模式描述以及介绍性叙述中提供与这些替代方案的链接。

Often, several alternative patterns could be used at each level. Of course, I almost always have a preference for which patterns to use, but one person's "anti-pattern" may be another person's "best practice pattern." As a result, this book includes patterns that I do not necessarily advocate. It describes the advantages and disadvantages of each of those patterns, allowing readers to make informed decisions about their use. I have tried to provide linkages to those alternatives in each of the pattern descriptions as well as in the introductory narratives.

模式的优点在于,它们提供了足够的信息,可以在多个备选方案之间做出明智的决定。我们选择的模式可能会受到测试自动化目标的影响。这些目标描述了测试自动化工作的预期结果。这些目标得到了许多原则的支持,这些原则编纂了关于什么使自动化测试“好”的信念体系。在本书中,测试自动化的目标在第 3 章“测试自动化目标”中描述,而原则在第 5 章测试自动化原则”中描述。

The nice thing about patterns is that they provide enough information to make an intelligent decision between several alternatives. The pattern we choose may be affected by the goals we have for test automation. The goals describe desired outcomes of the test automation efforts. These goals are supported by a number of principles that codify a belief system about what makes automated tests "good." In this book, the goals of test automation are described in Chapter 3, Goals of Test Automation, and the principles are described in Chapter 5, Principles of Test Automation.

最后一种模式更像是反模式[AP]。这些测试异味描述了反复出现的问题,我们的模式可以根据我们可能观察到的症状以及这些症状的根本原因来帮助我们解决这些问题。代码异味最早在 Martin Fowler 的书中[Ref]流行起来,并在 XP2001 [RTC]的一篇论文中作为测试异味应用于基于 xUnit 的测试。测试异味与可用于消除它们的模式以及更有可能导致它们的模式3相互引用。4此外,测试异味在其自己的章节中有深入的介绍:第二部分测试异味

The final kind of pattern is more of an anti-pattern [AP]. These test smells describe recurring problems that our patterns help us address in terms of the symptoms we might observe and the root causes of those symptoms. Code smells were first popularized in Martin Fowler's book [Ref] and applied to xUnit-based testing as test smells in a paper presented at XP2001 [RTC]. The test smells are cross-referenced with the patterns that can be used to banish them as well as the patterns3 that are more likely to lead to them.4 In addition, the test smells are covered in depth in their own section: Part II, The Test Smells.

图案形式

Pattern Form

本书收录了我对模式的描述。这些模式本身在我开始编目之前就已经存在了,因为它们是由至少三个不同的测试自动化人员独立发明的。我主动把它们写下来,以便让知识更容易传播。但要做到这一点,我必须选择一种模式描述形式。

This book includes my descriptions of patterns. The patterns themselves existed before I started cataloging them, by virtue of having been invented independently by at least three different test automaters. I took it upon myself to write them down as a way of making the knowledge more easily distributable. But to do so, I had to choose a pattern description form.

模式描述的形式和大小各有不同。有些模式的结构非常严格,由许多标题定义,帮助读者找到各个部分。有些模式读起来更像文学作品,但可能更难用作参考。尽管如此,所有模式都有一个共同的信息核心,无论它是如何呈现的。

Pattern descriptions come in many shapes and sizes. Some have a very rigid structure defined by many headings that help the reader find the various sections. Others read more like literature but may be more difficult to use as a reference. Nevertheless, all patterns have a common core of information, however it is presented.

我的图案形式

我非常喜欢阅读马丁·福勒的作品,而这种乐趣很大程度上归功于他所使用的格式。俗话说,“模仿是最真诚的奉承”:我厚颜无耻地抄袭了他的格式,只做了一些小改动。

I have really enjoyed reading the works of Martin Fowler, and I attribute much of that enjoyment to the pattern form that he uses. As the saying goes, "Imitation is the sincerest form of flattery": I have copied his format shamelessly with only a few minor modifications.

模板以问题陈述、总结陈述和草图开始。斜体问题陈述总结了模式所解决的问题的核心。它通常以问题的形式提出:“我们如何……”?粗体总结陈述用一两句话概括了模式的本质,而草图则提供了模式的视觉表示。草图后面紧接着的无标题文本部分用几句话总结了我们可能想要使用该模式的原因。它详细阐述了问题陈述,并包括传统模式模板中的“问题”和“上下文”部分。读者应该能够通过浏览此部分来了解他或她是否想继续阅读。

The template begins with the problem statement, the summary statement, and a sketch. The italicized problem statement summarizes the core of the problem that the pattern addresses. It is often stated as a question: "How do we . . . ?" The boldface summary statement captures the essence of the pattern in one or two sentences, while the sketch provides a visual representation of the pattern. The untitled section of text immediately after the sketch summarizes why we might want to use the pattern in just a few sentences. It elaborates on the problem statement and includes both the "Problem" and "Context" sections from the traditional pattern template. A reader should be able to get a sense of whether he or she wants to read any further by skimming this section.

接下来的三节提供了该模式的精髓。“工作原理”部分描述了该模式的结构和目的。当有几种方法可以实现该模式的某些重要方面时,它还包括有关“结果上下文”的信息。本节对应于更传统的模式形式的“解决方案”或“因此”部分。“何时使用它”部分描述了您应该考虑使用该模式的情况。本节对应于传统模式模板的“问题”、“力量”、“上下文”和“相关模式”部分。它还包括有关“结果上下文”的信息,这些信息可能会影响您是否要使用此模式。我还包括了可能建议您使用此模式的任何“测试味道”。“实施说明”部分描述了如何实施该模式的具体细节。本节中的小标题指出了模式的关键组件或可以如何实施该模式的变化。

The next three sections provide the meat of the pattern. The "How It Works" section describes the essence of how the pattern is structured and what it is about. It also includes information about the "resulting context" when there are several ways to implement some important aspect of the pattern. This section corresponds to the "Solution" or "Therefore" sections of more traditional pattern forms. The "When to Use It" section describes the circumstances in which you should consider using the pattern. This section corresponds to the "Problem," "Forces," "Context," and "Related Patterns" sections of traditional pattern templates. It also includes information about the "Resulting Context," when this information might affect whether you would want to use this pattern. I also include any "test smells" that might suggest that you should use this pattern. The "Implementation Notes" section describes the nuts and bolts of how to implement the pattern. Subheadings within this section indicate key components of the pattern or variations in how the pattern can be implemented.

大多数具体模式都包含三个附加部分。“激励示例”部分提供了在应用此模式之前测试代码可能是什么样子的示例。标题为“示例:{模式名称}”的部分显示了应用此模式后测试的样子。“重构说明”部分提供了有关如何从“激励示例”转到“示例:{模式名称}”的更详细说明。

Most of the concrete patterns include three additional sections. The "Motivating Example" section provides examples of what the test code might have looked like before this pattern was applied. The section titled "Example: {Pattern Name}" shows what the test would look like after the pattern was applied. The "Refactoring Notes" section provides more detailed instructions on how to get from the "Motivating Example" to the "Example: {Pattern Name}."

如果该模式在其他地方有记载,描述中可能会包含一个标题为“进一步阅读”的部分。当这些应用程序有特别有趣的内容时,就会出现“已知用途”部分。当然,这些模式中的大多数都已在许多系统中出现,因此挑选三种用途来证实它们是武断且毫无意义的。

If the pattern is written up elsewhere, the description may include a section titled "Further Reading." A "Known Uses" section appears when there is something particularly interesting about those applications. Most of these patterns have been seen in many systems, of course, so picking three uses to substantiate them would be arbitrary and meaningless.

如果存在多种相关技术,则通常将它们作为具有多种变体的单个模式介绍。如果变体是实现相同基本模式的不同方式(即以相同的一般方式解决相同的问题),则变体及其之间的差异将在“实现说明”部分列出。如果变体主要是使用该模式的不同原因,则变体将在“何时使用它”部分列出。

Where a number of related techniques exist, they are often presented here as a single pattern with several variations. If the variations are different ways to implement the same fundamental pattern (namely, solving the same problem the same general way), the variations and the differences between them are listed in the "Implementation Notes" section. If the variations are primarily a different reason for using the pattern, the variations are listed in the "When to Use It" section.

历史模式和气味

Historical Patterns and Smells

我曾努力想出一个足够简洁的模式和气味列表,同时尽可能保留历史名称。我经常将历史名称列为模式或气味的别名。在某些情况下,将模式的历史版本视为较大模式的特定变体更有意义。在这种情况下,我通常会将历史模式作为命名变体包含在“实施说明”部分中。

I struggled mightily when trying to come up with a concise enough list of patterns and smells while still keeping historical names whenever possible. I often list the historical name as an alias for the pattern or smell. In some cases, it made more sense to consider the historical version of the pattern as a specific variation of a larger pattern. In such a case, I usually include the historical pattern as a named variation in the "Implementation Notes" section.

许多历史气味没有通过“嗅探测试”——也就是说,气味描述的是根本原因而不是症状。5如果历史测试气味描述的是原因而不是症状,我选择将其移到相应的基于症状的气味中,作为一种特殊的变体,称为“原因”。神秘客人(请参阅第 186页的模糊测试)就是一个很好的例子。

Many of the historical smells did not pass the "sniff test"—that is, the smell described a root cause rather than a symptom.5 Where an historical test smell describes a cause and not a symptom, I have chosen to move it into the corresponding symptom-based smell as a special kind of variation titled "Cause". Mystery Guest (see Obscure Test on page 186) is a good example.

参照图案和气味

Referring to Patterns and Smells

我也努力想出一个好方法来引用模式和气味,特别是历史模式和气味。我希望能够在适当的时候使用历史名称和新的聚合名称,无论哪个更合适。我还希望读者能够看到哪个是哪个。在本书的在线版本中,超链接用于此目的。但是,对于印刷版本,我需要一种方法来将此链接表示为参考的页码注释,而不会使整个文本充斥着引用。经过几次尝试,我找到的解决方案包括第一次在章节、模式或气味中引用模式或气味时可以找到的页码。如果引用的是模式变体或气味的原因,我会在第一次引用时包含聚合模式或气味名称。请注意,对模糊测试的神秘客人原因的第二次引用没有气味名称,而对模糊测试的其他原因(如无关信息) (参见模糊测试)的引用包括聚合气味名称但不包含页码。

I also struggled to come up with a good way to refer to patterns and smells, especially the historical ones. I wanted to be able to use both the historical names when appropriate and the new aggregate names, whichever was more appropriate. I also wanted the reader to be able to see which was which. In the online version of this book, hyperlinks were used for this purpose. For the printed version, however, I needed a way to represent this linkage as a page number annotation of the reference without cluttering up the entire text with references. The solution I landed on after several tries includes the page number where the pattern or smell can be found the first time it is referenced in a chapter, pattern, or smell. If the reference is to a pattern variation or the cause of a smell, I include the aggregate pattern or smell name the first time. Note how this second reference to the Mystery Guest cause of Obscure Test shows up without the smell name, whereas references to other causes of Obscure Test such as Irrelevant Information (see Obscure Test) include the aggregate smell name but not the page number.

重构

Refactoring

重构是软件开发中一个相对较新的概念。虽然人们总是需要修改现有代码,但重构是一种高度规范的方法,可以在改变代码行为的情况下更改设计。它与自动化测试密切相关,因为如果没有自动化测试的安全网来证明您在重新设计期间没有破坏任何东西,就很难进行重构。

Refactoring is a relatively new concept in software development. While people have always had a need to modify existing code, refactoring is a highly disciplined approach to changing the design without changing the behavior of the code. It goes hand-in-hand with automated testing because it is very difficult to do refactoring without having the safety net of automated tests to prove that you have not broken anything during your redesign.

许多现代集成开发环境 (IDE) 都内置了对重构的支持。它们中的大多数都自动执行了 Martin Fowler 的书[Ref]中描述的至少几个重构步骤。不幸的是,这些工具并没有告诉我们何时或为什么要使用重构。为此,我们必须买一本 Martin 的书!关于这个主题的另一本必读书籍是 Joshua Kerievsky 的书[RtP]

Many of the modern integrated development environments (IDEs) have built-in support for refactoring. Most of them automate the refactoring steps of at least a few of the refactorings described in Martin Fowler's book [Ref]. Unfortunately, the tools do not tell us when or why we should use refactoring. We will have to get a copy of Martin's book for that! Another piece of mandatory reading on this topic is Joshua Kerievsky's book [RtP].

重构测试与重构生产代码略有不同,因为我们的自动化测试没有自动化测试!如果测试在重构后失败,失败是因为我们在重构过程中犯了错误吗?仅仅因为测试在测试重构后通过了,我们能确定它在适当的时候仍然会失败吗?为了解决这个问题,许多测试重构都是非常保守的“安全重构”,将引入测试行为变化​​的可能性降到最低。我们还尝试通过采用适当的测试策略来避免对测试进行大规模重构,如第 6 章测试自动化策略”中所述。

Refactoring tests differs a bit from refactoring production code because we do not have automated tests for our automated tests! If a test fails after a refactoring of the test, did the failure occur because we made a mistake during the refactoring? Just because a test passes after a test refactoring, can we be sure it will still fail when appropriate? To address this issue, many test refactorings are very conservative, "safe refactorings" that minimize the chance of introducing a change of behavior into the test. We also try to avoid having to do major refactorings of tests by adopting an appropriate test strategy, as described in Chapter 6, Test Automation Strategy.

本书更关注重构的目标,而不是重构的机制。附录 A中确实有重构的简短摘要,但重构过程不是本书的重点。这些模式本身很新,我们还没有时间就其名称、内容或适用性达成一致,更不用说就重构的最佳方式达成共识了。另一个复杂因素是,每个重构目标(模式)可能都有许多起点,如果试图提供详细的重构说明,这本已经很厚的书将变得更厚。

This book focuses more on the target of the refactoring than on the mechanics of this endeavor. A short summary of the refactorings does appear in Appendix A, but the process of refactoring is not the primary focus of this book. The patterns themselves are new enough that we have not yet had time to agree on their names, content, or applicability, let alone reach consensus on the best way to refactor to them. A further complication is that there are potentially many starting points for each refactoring target (pattern), and attempting to provide detailed refactoring instructions would make this already large book much larger.

假设

Assumptions

在撰写本书时,我假设读者对对象技术(也称为“面向对象编程”)有所了解;对象技术似乎是自动化单元测试流行的先决条件。这并不意味着我们不能用过程式或函数式语言进行测试,但使用这些语言可能会使测试更具挑战性(或至少有所不同)。

In writing this book, I assumed that the reader is somewhat familiar with object technology (also known as "object-oriented programming"); object technology seemed to be a prerequisite for automated unit testing to become popular. That does not mean we cannot perform testing in procedural or functional languages, but use of these languages may make it more challenging (or at least different).

不同的人有不同的学习风格。有些人需要从“大局”抽象开始,然后逐步深入到“足够”的细节。其他人只能理解细节,不需要“大局”。有些人通过听或读文字学习效果最好;其他人需要图片来帮助他们形象化概念。还有一些人通过阅读代码来最好地学习编程概念。我试图通过尽可能提供摘要、详细描述、代码示例和图片来适应所有这些学习风格。对于那些不会从这种学习方式中受益的读者来说,这些项目应该是可跳过的部分[PLOPD3] 。

Different people have different learning styles. Some need to start with the "big picture" abstractions and work down to "just enough" detail. Others can understand only the details and have no need for the "big picture." Some learn best by hearing or reading words; others need pictures to help them visualize a concept. Still others learn programming concepts best by reading code. I've tried to accommodate all of these learning styles by providing a summary, a detailed description, code samples, and a picture wherever possible. These items should be Skippable Sections [PLOPD3] for those readers who won't benefit from that style of learning.

术语

Terminology

本书汇集了两个不同领域的术语:软件开发和软件测试。因此,有些术语对某些读者来说不可避免地会不熟悉。读者在遇到任何不理解的术语时应参考词汇表。不过,我会在这里指出一两个术语,因为熟悉这些术语对于理解本书的大部分内容至关重要。

This book brings together terminology from two different domains: software development and software testing. As a consequence, some terminology will inevitably be unfamiliar to some readers. Readers should refer to the glossary when they encounter any terms that they do not understand. I will, however, point out one or two terms here, because becoming familiar with these terms is essential to understanding most of the material in this book.

测试术语

Testing Terminology

软件开发人员可能会发现“被测系统”(本书中简称为 SUT)这个术语不太熟悉。它是“我们正在测试的任何东西”的缩写。当我们编写单元测试时,SUT 是我们正在测试的任何类或方法;当我们编写客户测试时,SUT 可能是整个应用程序(或至少是它的一个主要子系统)。

Software developers will probably find the term "system under test" (abbreviated throughout this book as SUT) unfamiliar. It is short for "whatever thing we are testing." When we are writing unit tests, the SUT is whatever class or method(s) we are testing; when we are writing customer tests, the SUT is probably the entire application (or at least a major subsystem of it).

我们正在构建的应用程序或系统的任何部分,如果包含在 SUT 中,可能仍需要运行我们的测试,因为它被 SUT 调用,或者因为它设置了 SUT 在我们执行测试时将使用的先决条件数据。前一种元素称为依赖组件 (DOC) ,这两种类型都是测试装置的一部分。如图 I.1所示。

Any part of the application or system we are building that is not included in the SUT may still be required to run our test because it is called by the SUT or because it sets up prerequisite data that the SUT will use as we exercise it. The former type of element is called a depended-on component (DOC), and both types are part of the test fixture. This is illustrated in Figure I.1.

图 I.1。 一系列测试,每个测试都有自己的 SUT。应用程序、组件或单元只是特定测试集的 SUT。“Unit1 SUT”充当“Unit2 Test”的 DOC(夹具的一部分),是“Comp1 SUT”和“App1 SUT”的一部分。

Figure I.1. A range of tests each with its own SUT. An application, component, or unit is only the SUT with respect to a specific set of tests. The "Unit1 SUT" plays the role of DOC (part of the fixture) to "Unit2 Test" and is part of the "Comp1 SUT" and the "App1 SUT."

图像

特定语言的 xUnit 术语

Language-Specific xUnit Terminology

尽管本书包含各种语言和 xUnit 家族成员的示例,但JUnit在本书中占据了突出地位。JUnit 是大多数人至少有点熟悉的语言和 xUnit 框架。许多 JUnit 到其他语言的翻译都是相对忠实的移植,只需要对类和方法名称进行微小更改以适应底层语言的差异。如果不是这种情况,附录 BxUnit 术语交叉引用)通常包含适当的映射。

Although this book includes examples in a variety of languages and xUnit family members, JUnit figures prominently in this coverage. JUnit is the language and xUnit framework that most people are at least somewhat familiar with. Many of the translations of JUnit to other languages are relatively faithful ports, with only minor changes in class and method names needed to accommodate the differences in the underlying language. Where this isn't the case, Appendix B, xUnit Terminology Cross-Reference, often includes the appropriate mapping.

使用 Java 作为主要示例语言还意味着,在某些讨论中,我们将引用方法的 JUnit 名称,而不会列出每个 xUnit 框架中的相应方法名称。例如,讨论可能会引用 JUnit 的assertTrue方法,但不会提及NUnit等效项是Assert.IsTrueSUnit等效项是should:以及VbUnit等效项是verify。希望读者在脑海中将方法名称转换为他们可能最熟悉的SUnit、VbUnit、 Test::Unit和其他等效项。JUnit 方法的意图揭示名称[SBPP]应该足够清晰,以便我们进行讨论。

Using Java as the main sample language also means that in some discussions we will refer to the JUnit name of a method and will not list the corresponding method names in each of the xUnit frameworks. For example, a discussion may refer to JUnit's assertTrue method without mentioning that the NUnit equivalent is Assert.IsTrue, the SUnit equivalent is should:, and the VbUnit equivalent is verify. Readers are expected to do the mental swap of method names to the SUnit, VbUnit, Test::Unit, and other equivalents with which they may be most familiar. The Intent-Revealing Names [SBPP] of the JUnit methods should be clear enough for the purposes of our discussion.

代码示例

Code Samples

示例代码始终是个问题。来自真实项目的代码示例通常太大而无法包含,并且通常受禁止发布的保密协议的约束。“玩具程序”得不到太多尊重,因为“它们不是真实的”。像本书这样的书除了使用“玩具程序”外别无选择,但我已尝试使它们尽可能地代表真实项目。

Sample code is always a problem. Samples of code from real projects are typically much too large to include and are usually covered by nondisclosure agreements that preclude their publication. "Toy programs" do not get much respect because "they aren't real." A book such as this one has little choice except to use "toy programs," but I have tried to make them as representative as possible of real projects.

这里介绍的代码示例几乎全部来自“真实”的可编译和可执行代码,因此除非是在编辑过程中引入的,否则它们不应该(敲木头)包含任何编译错误。大多数 Ruby 示例来自我用来编写本书的基于 XML 的发布系统,而许多 Java 和 C# 示例来自我们在 ClearStream 用来向 ClearStream 客户教授这些概念的课件。

Almost all of the code samples presented here came from "real" compilable and executable code, so they should not (knock on wood) contain any compile errors unless they were introduced during the editing process. Most of the Ruby examples come from the XML-based publishing system I used to prepare this book, while many of the Java and C# samples came from courseware that we use at ClearStream to teach these concepts to ClearStream's clients.

我尝试使用多种语言来说明这些模式在 xUnit 系列成员中的普遍应用。在某些情况下,由于语言或 xUnit 系列成员的特定功能,特定模式决定了语言的使用。在其他情况下,语言是由 xUnit 系列特定成员的第三方扩展可用性决定的。否则,示例的默认语言是 Java 和一些 C#,因为大多数人至少对它们有阅读水平的熟悉。

I have tried to use a variety of languages to illustrate the nearly universal application of the patterns across the members of the xUnit family. In some cases, the specific pattern dictated the use of language because of specific features of either the language or the xUnit family member. In other cases, the language was dictated by the availability of third-party extensions for a specific member of the xUnit family. Otherwise, the default language for examples is Java with some C# because most people have at least reading-level familiarity with them.

由于建议的行长仅为 65 个字符,因此为书籍格式化代码是一项特别的挑战。我采取了一些自由度来缩短变量和类名,只是为了减少换行的行数。我还发明了一些换行惯例,以尽量减少这些示例的垂直尺寸。您可以放心,您的测试代码看起来应该比我的“短”得多,因为您需要换行的行数要少得多!

Formatting code for a book is a particular challenge due to the recommended line length of just 65 characters. I have taken some liberties in shortening variable and class names simply to reduce the number of lines that wrap. I've also invented some line-wrapping conventions to minimize the vertical size of these samples. You can take solace in the fact that your test code should look a lot "shorter" than mine because you have to wrap many fewer lines!

图表符号

Diagramming Notation

“一图胜千言”。只要有可能,我都会尝试包含每个模式或气味的草图。我的草图大致基于统一建模语言 ( UML ),但做了一些改动,使它们更具表现力。例如,我使用 UML 类图的聚合符号(菱形)和继承符号(三角形),但我将类和对象以及关联和对象交互混合在同一张图上。大多数符号在第19 章xUnit基础模式的模式中介绍,因此您可能会发现浏览本章只是为了看看图片是值得的。

"A picture is worth a thousand words." Wherever possible, I have tried to include a sketch of each pattern or smell. I've based the sketches loosely on the Unified Modeling Language (UML) but took a few liberties to make them more expressive. For example, I use the aggregation symbol (diamond) and the inheritance symbol (a triangle) of UML class diagrams, but I mix classes and objects on the same diagram along with associations and object interactions. Most of the notation is introduced in the patterns in Chapter 19, xUnit Basics Patterns, so you may find it worthwhile to skim this chapter just to look at the pictures.

尽管我试图通过比较草图使这种符号“可发现”,但还是需要指出一些惯例。对象有阴影;类和方法没有。类有方角,与 UML 保持一致;方法有圆角。大感叹号是断言(潜在的测试失败),星爆是引发的错误或异常。夹具是一朵云,反映了它的模糊性质,SUT 所依赖的任何组件都叠加在云上。草图试图说明的内容用较粗的线条和较暗的阴影突出显示。因此,您应该能够比较两个相关概念的草图,并快速确定每个草图中强调的内容。

Although I have tried to make this notation "discoverable" simply through comparing sketches, a few conventions are worth pointing out. Objects have shadows; classes and methods do not. Classes have square corners, in keeping with UML; methods have round corners. Large exclamation marks are assertions (potential test failures), and a starburst is an error or exception being raised. The fixture is a cloud, reflecting its nebulous nature, and any components the SUT depends on are superimposed on the cloud. Whatever the sketch is trying to illustrate is highlighted with heavier lines and darker shading. As a result, you should be able to compare two sketches of related concepts and quickly determine what is emphasized in each.

限制

Limitations

当您使用这些模式时,请记住,我不可能见过每一个测试自动化问题和每一个问题的解决方案;可能还有其他更好的方法来解决这些问题。这些解决方案只是对我和我交流过的人来说有效的解决方案。请谨慎接受每个人的建议!

As you use these patterns, please keep in mind that I could not have seen every test automation problem and every solution to every problem; there may well be other, possibly better, ways to solve some of these problems. These solutions are just the ones that have worked for me and for the people I have been communicating with. Accept everyone's advice with a grain of salt!

我希望这些模式能为您编写良好、强大的自动化测试提供一个起点。幸运的话,您将避免我们在第一次尝试时犯下的许多错误,并将继续发明更好的自动化测试方法。我很想听听你们的想法!

My hope is that these patterns will give you a starting point for writing good, robust automated tests. With luck, you will avoid many of the mistakes we made on our first attempts and will go on to invent even better ways of automating tests. I'd love to hear about them!

重构测试

Refactoring a Test

 

为什么要重构测试?

Why Refactor Tests?

测试很快就会成为敏捷开发过程中的瓶颈。对于那些从未体验过简单、易懂的测试与复杂、难懂、难以维护的测试之间的区别的人来说,这一点可能不是立即显而易见的。生产力差异可能是惊人的!

Tests can quickly become a bottleneck in an agile development process. This may not be immediately obvious to those who have never experienced the difference between simple, easily understood tests and complex, obtuse, hard-to-maintain tests. The productivity difference can be staggering!

本书的这一部分充当了整本书的“激励示例”,向您展示了重构测试可以带来多大的不同。它引导您完成一个示例,从一个复杂的测试开始,逐步将其重构为一个简单、易于理解的测试。在此过程中,我将指出一些关键的异味以及我们可以用来消除它们的模式。理想情况下,这个练习会激发您对更多内容的兴趣。

This section of the book acts as a "motivating example" for the entire book by showing you how much of a difference refactoring tests can make. It walks you through an example starting with a complex test and, step by step, refactors it to a simple, easily understood test. Along the way, I will point out some key smells and the patterns that we can use to remove them. Ideally, this exercise will whet your appetite for more.

复杂的测试

A Complex Test

下面是一个与我在各个项目中见过的一些测试类似的测试:

Here is a test that is not atypical of some of the tests I have seen on various projects:

public void testAddItemQuantity_severalQuantity_v1(){

     Address billingAddress = null;

     Address shippingAddress = null;

     Customer customer = null;

     Product product = null;

     Invoice invoice = null;

     try {

           // 设置夹具

           billingAddress = new Address("1222 1st St SW",

                   "Calgary", "Alberta", "T2N 2V2","Canada");

           shippingAddress = new Address("1333 1st St SW",

                   "Calgary", "Alberta", "T2N 2V2", "Canada");

           customer = new Customer(99, "John", "Doe",

                                                       new BigDecimal("30"),

                                                       billingAddress,

                                                       shippingAddress);

           product = new Product(88, "SomeWidget",

                                                 new BigDecimal("19.99"));

           invoice = new Invoice(customer);

           // 练习 SUT

           invoice.addItemQuantity(product, 5);

           // 验证结果

           列表 lineItems = invoice.getLineItems();

           if (lineItems.size() == 1) {

                LineItem actItem = (LineItem) lineItems.get(0);

                assertEquals("inv", invoice, actItem.getInv());

                assertEquals("prod", product, actItem.getProd());

                assertEquals("quant", 5, actItem.getQuantity());

                assertEquals("折扣", new BigDecimal("30"),

                                       actItem.getPercentDiscount());

                assertEquals("单价",new BigDecimal("19.99"),

                                           actItem.getUnitPrice());

                 assertEquals("扩展", new BigDecimal("69.96"),

                                        actItem.getExtendedPrice());

           } else {

                 assertTrue("发票应有 1 个项目", false);

           }

   } finally {

         // 拆卸

         deleteObject(invoice);

         deleteObject(product);

         deleteObject(customer);

         deleteObject(billingAddress);

         删除对象(运输地址);

    }

}

public  void  testAddItemQuantity_severalQuantity_v1(){

     Address  billingAddress  =  null;

     Address  shippingAddress  =  null;

     Customer  customer  =  null;

     Product  product  =  null;

     Invoice  invoice  =  null;

     try  {

           //  Set  up  fixture

           billingAddress  =  new  Address("1222  1st  St  SW",

                   "Calgary",  "Alberta",  "T2N  2V2","Canada");

           shippingAddress  =  new  Address("1333  1st  St  SW",

                   "Calgary",  "Alberta",  "T2N  2V2",  "Canada");

           customer  =  new  Customer(99,  "John",  "Doe",

                                                       new  BigDecimal("30"),

                                                       billingAddress,

                                                       shippingAddress);

           product  =  new  Product(88,  "SomeWidget",

                                                 new  BigDecimal("19.99"));

           invoice  =  new  Invoice(customer);

           //  Exercise  SUT

           invoice.addItemQuantity(product,  5);

           //  Verify  outcome

           List  lineItems  =  invoice.getLineItems();

           if  (lineItems.size()  ==  1)  {

                LineItem  actItem  =  (LineItem)  lineItems.get(0);

                assertEquals("inv",  invoice,  actItem.getInv());

                assertEquals("prod",  product,  actItem.getProd());

                assertEquals("quant",  5,  actItem.getQuantity());

                assertEquals("discount",  new  BigDecimal("30"),

                                       actItem.getPercentDiscount());

                assertEquals("unit  price",new  BigDecimal("19.99"),

                                           actItem.getUnitPrice());

                 assertEquals("extended",  new  BigDecimal("69.96"),

                                        actItem.getExtendedPrice());

           }  else  {

                 assertTrue("Invoice  should  have  1  item",  false);

           }

   }  finally  {

         //  Teardown

         deleteObject(invoice);

         deleteObject(product);

         deleteObject(customer);

         deleteObject(billingAddress);

         deleteObject(shippingAddress);

    }

}

 

这个测试相当长1,而且比它需要的要复杂得多。这个模糊测试第 186页)很难理解,因为测试中的行数太多,让人很难看清全局。它还存在许多其他问题,我们将分别解决这些问题。

This test is quite long 1 and is much more complicated than it needs to be. This Obscure Test (page 186) is difficult to understand because the sheer number of lines in the test makes it hard to see the big picture. It also suffers from a number of other problems that we will address individually.

清理测试

Cleaning Up the Test

让我们看一下测试的各个部分。

Let's look at each of the various parts of the test.

清理验证逻辑

首先,我们来关注一下验证预期结果的部分。也许我们可以从断言中推断出这个测试试图验证哪些测试条件。

First, let's focus on the part that verifies the expected outcome. Maybe we can infer from the assertions which test conditions this test is trying to verify.

列表 lineItems = invoice.getLineItems();

if (lineItems.size() == 1) {

     LineItem actItem = (LineItem) lineItems.get(0);

     assertEquals("inv", invoice, actItem.getInv());

     assertEquals("prod", product, actItem.getProd());

     assertEquals("quant", 5, actItem.getQuantity());

     assertEquals("折扣", new BigDecimal("30"),

                            actItem.getPercentDiscount());

     assertEquals("单价",new BigDecimal("19.99"),

                                 actItem.getUnitPrice());

     assertEquals("扩展", new BigDecimal("69.96"),

                            actItem.getExtendedPrice());

} else {

      assertTrue("发票应有 1 个项目", false);

}

List  lineItems  =  invoice.getLineItems();

if  (lineItems.size()  ==  1)  {

     LineItem  actItem  =  (LineItem)  lineItems.get(0);

     assertEquals("inv",  invoice,  actItem.getInv());

     assertEquals("prod",  product,  actItem.getProd());

     assertEquals("quant",  5,  actItem.getQuantity());

     assertEquals("discount",  new  BigDecimal("30"),

                            actItem.getPercentDiscount());

     assertEquals("unit  price",new  BigDecimal("19.99"),

                                 actItem.getUnitPrice());

     assertEquals("extended",  new  BigDecimal("69.96"),

                            actItem.getExtendedPrice());

}  else  {

      assertTrue("Invoice  should  have  1  item",  false);

}

 

一个简单的问题就是最后一行的断言。使用assertTrue参数调用false总是会导致测试失败,那么我们为什么不直接说出来呢?让我们将其改为调用fail

A simple problem to fix is the obtuse assertion on the very last line. Calling assertTrue with an argument of false should always result in a test failure, so why don't we say so directly? Let's change this to a call to fail:

列表 lineItems = invoice.getLineItems();

if (lineItems.size() == 1) {

     LineItem actItem = (LineItem) lineItems.get(0);

     assertEquals("inv", invoice, actItem.getInv());

     assertEquals("prod", product, actItem.getProd());

     assertEquals("quant", 5, actItem.getQuantity());

     assertEquals("discount", new BigDecimal("30"),

                            actItem.getPercentDiscount());

     assertEquals("unit price",new BigDecimal("19.99"),

                                actItem.getUnitPrice());

     assertEquals("extended", new BigDecimal("69.96"),

                            actItem.getExtendedPrice());

} else {

      fail("发票应该只有一个行项目");

}

List  lineItems  =  invoice.getLineItems();

if  (lineItems.size()  ==  1)  {

     LineItem  actItem  =  (LineItem)  lineItems.get(0);

     assertEquals("inv",  invoice,  actItem.getInv());

     assertEquals("prod",  product,  actItem.getProd());

     assertEquals("quant",  5,  actItem.getQuantity());

     assertEquals("discount",  new  BigDecimal("30"),

                            actItem.getPercentDiscount());

     assertEquals("unit  price",new  BigDecimal("19.99"),

                                actItem.getUnitPrice());

     assertEquals("extended",  new  BigDecimal("69.96"),

                            actItem.getExtendedPrice());

}  else  {

      fail("Invoice  should  have  exactly  one  line  item");

}

 

我们可以将此举视为一种提取方法 [Fowler] 重构,因为我们正在用一个硬编码参数替换陈述结果断言(请参阅第 362页的断言方法),该参数具有对封装该调用的单一结果断言(请参阅断言方法)方法的更能揭示意图的调用。

We can think of this move as an Extract Method [Fowler] refactoring, because we are replacing the Stated Outcome Assertion (see Assertion Method on page 362) with a hard-coded parameter with a more intent-revealing call to a Single Outcome Assertion (see Assertion Method) method that encapsulates the call.

当然,这组断言还存在其他一些问题。例如,我们为什么需要这么多断言?事实证明,这些断言中有许多是测试由 的构造函数设置的字段,LineItem而该构造函数本身已被另一个单元测试覆盖。那么为什么要在这里重复这些断言呢?当逻辑发生变化时,它只会创建更多需要维护的测试代码。

Of course, this set of assertions suffers from several more problems. For example, why do we need so many of them? It turns out that many of these assertions are testing fields set by the constructor for the LineItem, which is itself covered by another unit test. So why repeat these assertions here? It will just create more test code to maintain when the logic changes.

一种解决方案是针对预期对象使用单个断言(请参阅第462页的状态验证),而不是每个对象字段使用一个断言。首先,我们定义一个对象,该对象看起来与我们期望的结果完全一致。在本例中,我们创建一个预期,其中的字段填充了预期值,包括从初始化的和。LineItemunitPriceextendedPriceproduct

One solution is to use a single assertion on an Expected Object (see State Verification on page 462) instead of one assertion per object field. First, we define an object that looks exactly how we expect the result to look. In this case, we create an expected LineItem with the fields filled in with the expected values, including the unitPrice and extendedPrice initialized from the product.

列表 lineItems = invoice.getLineItems();

if (lineItems.size() == 1) {

     LineItem expected =

          new LineItem(invoice, product, 5,

                                 new BigDecimal("30"),

                                 new BigDecimal("69.96"));

     LineItem actItem = (LineItem) lineItems.get(0);

     assertEquals("发票", expected.getInv(),

                                        actItem.getInv());

     assertEquals("产品", expected.getProd(),

                                        actItem.getProd());

     assertEquals("数量",expected.getQuantity(),

                                        actItem.getQuantity());

     assertEquals("折扣",

                            expected.getPercentDiscount(),

                            actItem.getPercentDiscount());

     assertEquals("单位 pr", new BigDecimal("19.99"),

                                        actItem.getUnitPrice());

     assertEquals("extend pr",new BigDecimal("69.96"),

                                        actItem.getExtendedPrice());

} else {

      fail("发票应该只有一行项目");

}

List  lineItems  =  invoice.getLineItems();

if  (lineItems.size()  ==  1)  {

     LineItem  expected  =

          new  LineItem(invoice,  product,  5,

                                 new  BigDecimal("30"),

                                 new  BigDecimal("69.96"));

     LineItem  actItem  =  (LineItem)  lineItems.get(0);

     assertEquals("invoice",  expected.getInv(),

                                        actItem.getInv());

     assertEquals("product",  expected.getProd(),

                                        actItem.getProd());

     assertEquals("quantity",expected.getQuantity(),

                                        actItem.getQuantity());

     assertEquals("discount",

                            expected.getPercentDiscount(),

                            actItem.getPercentDiscount());

     assertEquals("unit  pr",  new  BigDecimal("19.99"),

                                        actItem.getUnitPrice());

     assertEquals("extend  pr",new  BigDecimal("69.96"),

                                        actItem.getExtendedPrice());

}  else  {

      fail("Invoice  should  have  exactly  one  line  item");

}

 

一旦我们创建了预期对象,我们就可以使用以下方法对其进行断言assertEquals

Once we have created our Expected Object, we can then assert on it using assertEquals:

列表 lineItems = invoice.getLineItems();

if (lineItems.size() == 1) {

     LineItem expected =

          new LineItem(invoice, product,5,

                                  new BigDecimal("30"),

                                  new BigDecimal("69.96"));

     LineItem actItem = (LineItem) lineItems.get(0);

     assertEquals("invoice", expected, actItem);

} else {

      fail("发票应该只有一个行项目");

}

List  lineItems  =  invoice.getLineItems();

if  (lineItems.size()  ==  1)  {

     LineItem  expected  =

          new  LineItem(invoice,  product,5,

                                  new  BigDecimal("30"),

                                  new  BigDecimal("69.96"));

     LineItem  actItem  =  (LineItem)  lineItems.get(0);

     assertEquals("invoice",  expected,  actItem);

}  else  {

      fail("Invoice  should  have  exactly  one  line  item");

}

 

显然,保留整个对象 [Fowler] 重构使代码变得更加简单和明显。但是等等!为什么测试中需要语句?如果测试有多条路径,我们如何知道实际执行的是哪一条?如果我们可以消除这个条件测试逻辑第 200if页),那就好多了。幸运的是,模式Guard Assertion第 490页)就是为处理这种情况而设计的。我们只需使用用 Guard Clause 替换条件 [Fowler] 重构来将. . . 序列替换为对相同条件的断言。如果不满足条件,此Guard Assertion就会暂停执行,而无需引入条件测试逻辑if  ...  else  fail()

Clearly, the Preserve Whole Object [Fowler] refactoring makes the code a lot simpler and more obvious. But wait! Why do we have an if statement in a test? If there are several paths through a test, how do we know which one is actually being executed? It would be a lot better if we could eliminate this Conditional Test Logic (page 200). Luckily for us, the pattern Guard Assertion (page 490) is designed to handle exactly this case. We simply use a Replace Conditional with Guard Clause [Fowler] refactoring to replace the if  ...  else  fail() . . . sequence with an assertion on the same condition. This Guard Assertion halts execution if the condition is not met without introducing Conditional Test Logic.

列表 lineItems = invoice.getLineItems();

assertEquals("项目数量", 1, lineItems.size());

LineItem expected =

      new LineItem(invoice, product, 5,

                              new BigDecimal("30"),

                              new BigDecimal("69.96"));

LineItem actItem = (LineItem) lineItems.get(0);

assertEquals("发票", expected, actItem);

List  lineItems  =  invoice.getLineItems();

assertEquals("number  of  items",  1,  lineItems.size());

LineItem  expected  =

      new  LineItem(invoice,  product,  5,

                              new  BigDecimal("30"),

                              new  BigDecimal("69.96"));

LineItem  actItem  =  (LineItem)  lineItems.get(0);

assertEquals("invoice",  expected,  actItem);

 

到目前为止,我们已将 11 行验证代码缩减为仅 4 行,而且这 4 行代码非常简单。2有些人可能会认为这种重构已经足够好了。但是,我们不能让这个断言更加明显吗?我们真正想要验证的是什么?我们试图说应该只有一行项目,并且它应该看起来与我们的完全一样expectedLineItem。我们可以通过使用提取方法重构来定义自定义断言第 474页)来明确地说出这一点。

So far, we have reduced 11 lines of verification code to just 4, and those 4 lines are a lot simpler code to boot.2 Some people might suggest that this refactoring is good enough. But can't we make this assertion even more obvious? What are we really trying to verify? We are trying to say that there should be only one line item and it should look exactly like our expectedLineItem. We can say this explicitly by using an Extract Method refactoring to define a Custom Assertion (page 474).

LineItem 预期 =

      新 LineItem (发票,产品,5,

                             新 BigDecimal ("30"),

                             新 BigDecimal ("69.96"));

assertContainsExactlyOneLineItem (发票,预期);

LineItem  expected  =

      new  LineItem(invoice,  product,  5,

                             new  BigDecimal("30"),

                             new  BigDecimal("69.96"));

assertContainsExactlyOneLineItem(invoice,  expected);

 

好多了!现在我们将测试的验证部分缩减为仅两行。让我们回顾一下整个测试的样子:

That is better! Now we have the verification part of the test down to just two lines. Let's review what the whole test looks like:

public void testAddItemQuantity_severalQuantity_v6(){

    Address billingAddress = null;

    Address shippingAddress = null;

    Customer customer = null;

    Product product = null;

    Invoice invoice = null;

    try {

        // 设置夹具

        billingAddress = new Address("1222 1st St SW",

                  "Calgary", "Alberta", "T2N 2V2", "Canada");

        shippingAddress = new Address("1333 1st St SW",

                  "Calgary", "Alberta", "T2N 2V2", "Canada");

        customer = new Customer(99, "John", "Doe",

                                                    new BigDecimal("30"),

                                                    billingAddress,

                                                    shippingAddress);

        product = new Product(88, "SomeWidget",

                                                new BigDecimal("19.99"));

        invoice = new Invoice(customer);

        // 练习 SUT

        invoice.addItemQuantity(product, 5);

        // 验证结果

        LineItem expected =

            new LineItem(invoice, product, 5,

                                   new BigDecimal("30"),

                                   new BigDecimal("69.96"));

        assertContainsExactlyOneLineItem(invoice, expected);

   } finally {

        // 拆卸

        deleteObject(invoice);

        deleteObject(product);

        deleteObject(customer);

        deleteObject(billingAddress);

        deleteObject(shippingAddress);

   }

}

public  void  testAddItemQuantity_severalQuantity_v6(){

    Address  billingAddress  =  null;

    Address  shippingAddress  =  null;

    Customer  customer  =  null;

    Product  product  =  null;

    Invoice  invoice  =  null;

    try  {

        //  Set  up  fixture

        billingAddress  =  new  Address("1222  1st  St  SW",

                  "Calgary",  "Alberta",  "T2N  2V2",  "Canada");

        shippingAddress  =  new  Address("1333  1st  St  SW",

                  "Calgary",  "Alberta",  "T2N  2V2",  "Canada");

        customer  =  new  Customer(99,  "John",  "Doe",

                                                    new  BigDecimal("30"),

                                                    billingAddress,

                                                    shippingAddress);

        product  =  new  Product(88,  "SomeWidget",

                                                new  BigDecimal("19.99"));

        invoice  =  new  Invoice(customer);

        //  Exercise  SUT

        invoice.addItemQuantity(product,  5);

        //  Verify  outcome

        LineItem  expected  =

            new  LineItem(invoice,  product,  5,

                                   new  BigDecimal("30"),

                                   new  BigDecimal("69.96"));

        assertContainsExactlyOneLineItem(invoice,  expected);

   }  finally  {

        //  Teardown

        deleteObject(invoice);

        deleteObject(product);

        deleteObject(customer);

        deleteObject(billingAddress);

        deleteObject(shippingAddress);

   }

}

 

清理 Fixture 拆卸逻辑

Cleaning Up the Fixture Teardown Logic

现在我们已经清理了结果验证逻辑,让我们把注意力转向finally测试末尾的块。这段代码在做什么?

Now that we have cleaned up the result verification logic, let's turn our attention to the finally block at the end of the test. What is this code doing?

} 最后 {

        // 拆卸

        deleteObject(发票);

        deleteObject(产品);

        deleteObject(客户);

        deleteObject(账单地址);

        deleteObject(发货地址);

}

}  finally  {

        //  Teardown

        deleteObject(invoice);

        deleteObject(product);

        deleteObject(customer);

        deleteObject(billingAddress);

        deleteObject(shippingAddress);

}

 

大多数现代语言都有与块等效的构造try/finally,可用于确保即使发生错误或异常,代码也能运行。在测试方法(第 348页) 中,finally块可确保无论测试通过与否,任何清理代码都能运行。失败的断言会引发异常,这会将控制权转回测试自动化框架(第 298页) 的异常处理代码,因此我们finally首先使用块进行清理。这种方法意味着我们不必捕获异常然后重新抛出它。

Most modern languages have an equivalent construct to the try/finally block that can be used to ensure that code gets run even when an error or exception occurs. In a Test Method (page 348), the finally block ensures that any cleanup code gets run regardless of whether the test passed or failed. A failed assertion throws an exception, which would transfer control back to the Test Automation Framework's (page 298) exception-handling code, so we use the finally block to clean up first. This approach means that we avoid having to catch the exception and then rethrow it.

在此测试中,finally块调用deleteObject测试创建的每个对象上的方法。不幸的是,此代码存在致命缺陷。您注意到了吗?

In this test, the finally block calls the deleteObject method on each of the objects created by the test. Unfortunately, this code suffers from a fatal flaw. Have you noticed it yet?

在拆卸过程中可能会出错。如果第一次调用deleteObject抛出异常会发生什么?正如这里所编码的那样,其他对的调用都deleteObject不会执行。解决方案是在第一次调用周围使用嵌套try/finally块,从而确保第二次对的调用deleteObject始终执行。但如果第二次调用失败怎么办?在这种情况下,我们需要总共六个嵌套try/finally块才能使此操作成功。这几乎会使测试长度增加一倍,我们无法承受在每个测试中编写和维护如此多的代码。

Things could go wrong during the teardown itself. What happens if the first call to deleteObject throws an exception? As coded here, none of the other calls to deleteObject would be executed. The solution is to use a nested try/finally block around this first call, thereby ensuring that the second call to deleteObject always executes. But what if the second call fails? In this case, we would need a total of six nested try/finally blocks to make this maneuver work. That would almost double the length of the test, and we cannot afford to write and maintain so much code in each test.

} finally {

      // 拆卸

      尝试 {

          deleteObject(invoice);

      } finally {

           尝试 {

               deleteObject(product);

           } finally {

                尝试 {

                    deleteObject(customer);

                } finally {

                      尝试 {

                          deleteObject(billingAddress);

                      } finally {

                           deleteObject(shippingAddress);

                      }

                }

           }

      }

}  finally  {

      //        Teardown

      try  {

          deleteObject(invoice);

      }  finally  {

           try  {

               deleteObject(product);

           }  finally  {

                try  {

                    deleteObject(customer);

                }  finally  {

                      try  {

                          deleteObject(billingAddress);

                      }  finally  {

                           deleteObject(shippingAddress);

                      }

                }

           }

      }

 

问题是我们现在有一个复杂的拆卸(参见条件测试逻辑)。这段代码正确的几率有多大?我们如何测试测试代码?显然,我们目前的方法不会很有效。

The problem is that we now have a Complex Teardown (see Conditional Test Logic). What are the chances of getting this code right? And how do we test the test code? Clearly, our current approach is not going to be very effective.

当然,我们可以将这段代码移到tearDown方法中。这样做的好处是可以将其从测试方法中移除。此外,由于该tearDown方法充当块finally,我们可以摆脱最外层的try/finally。不幸的是,这种策略并没有解决问题的根源:需要在每个测试中编写详细的拆卸代码。

Of course, we could move this code into the tearDown method. That would have the advantage of removing it from the Test Method. Also, because the tearDown method acts as a finally block, we would get rid of the outermost try/finally. Unfortunately, this strategy doesn't address the root of the problem: the need to write detailed teardown code in each test.

我们可以尝试通过使用在测试之间不会被拆除的共享装置第 317页)来避免创建对象。不幸的是,这种方法可能会导致许多测试异味,包括不可重复的测试(请参阅第228页的不稳定测试)和交互测试(请参阅不稳定测试),这些异味是由通过共享装置进行的交互引起的。另一个问题是,对共享装置中使用的对象的引用通常是神秘的客人(请参阅模糊的测试。3

We could try to avoid creating the objects in the first place by using a Shared Fixture (page 317) that is not torn down between tests. Unfortunately, this approach is likely to lead to a number of test smells, including Unrepeatable Test (see Erratic Test on page 228) and Interacting Tests (see Erratic Test), caused by interactions via the shared fixture. Another issue is that the references to objects used from the shared fixture are often Mystery Guests (see Obscure Test).3

最好的解决方案是使用Fresh Fixture第 311页),但要避免为每个测试编写拆卸代码。为此,我们可以使用自动垃圾收集的内存装置。但是,如果我们创建的对象是持久性的(例如,如果它们保存在数据库中),则此方法将不起作用。虽然最好构建系统架构,以便我们的大多数测试可以在没有数据库的情况下执行,但我们几乎总是有一些测试需要它。在这些情况下,我们可以扩展测试自动化框架来为我们完成大部分工作。我们可以添加一种方法来将我们创建的每个对象注册到框架中,以便它可以为我们完成删除。

The best solution is to use a Fresh Fixture (page 311) but to avoid writing teardown code for every test. To do so, we can use an in-memory fixture that is automatically garbage collected. This approach won't work, however, if the objects we create are persistent (e.g., if they are saved in a database). While it is best to construct the system architecture so that most of our tests can be executed without the database, we almost always have some tests that need it. In these cases, we can extend the Test Automation Framework to do most of the work for us. We can add a means to register each object we create with the framework so that it can do the deleting for us.

首先,我们需要在创建每个对象时对其进行注册:

First, we need to register each object as we create it:

// 设置夹具

billingAddress = new Address("1222 1st St SW", "Calgary",

                              "Alberta", "T2N 2V2", "Canada");

registerTestObject(billingAddress);

shippingAddress = new Address("1333 1st St SW", "Calgary",

                                "Alberta","T2N 2V2", "Canada");

registerTestObject(shippingAddress);

customer = new Customer(99, "John", "Doe",

                                             new BigDecimal("30"),

                                             billingAddress,

                                             shippingAddress);

registerTestObject(shippingAddress);

product = new Product(88, "SomeWidget",

                                         new BigDecimal("19.99"));

registerTestObject(shippingAddress);

invoice = new Invoice(customer);

registerTestObject(shippingAddress);

//    Set  up  fixture

billingAddress  =  new  Address("1222  1st  St  SW",  "Calgary",

                              "Alberta",  "T2N  2V2",  "Canada");

registerTestObject(billingAddress);

shippingAddress  =  new  Address("1333  1st  St  SW",  "Calgary",

                                "Alberta","T2N  2V2",  "Canada");

registerTestObject(shippingAddress);

customer  =  new  Customer(99,  "John",  "Doe",

                                             new  BigDecimal("30"),

                                             billingAddress,

                                             shippingAddress);

registerTestObject(shippingAddress);

product  =  new  Product(88,  "SomeWidget",

                                         new  BigDecimal("19.99"));

registerTestObject(shippingAddress);

invoice  =  new  Invoice(customer);

registerTestObject(shippingAddress);

 

注册包括将对象添加到测试对象集合中:

Registration consists of adding the object to a collection of test objects:

列出 testObjects;



protected void setUp() 抛出异常 {

      super.setUp();

      testObjects = new ArrayList();

}



protected void registerTestObject(Object testObject) {

      testObjects.add(testObject);

}

List  testObjects;



protected  void  setUp()  throws  Exception  {

      super.setUp();

      testObjects  =  new  ArrayList();

}



protected  void  registerTestObject(Object  testObject)  {

      testObjects.add(testObject);

}

 

在该tearDown方法中,我们遍历测试对象列表并删除每个测试对象:

In the tearDown method, we iterate through the list of test objects and delete each one:

public void teadown() {

      Iterator i = testObjects.iterator();

      while (i.hasNext()) {

            try {

                  deleteObject(i.next());

            } catch (RuntimeException e) {

                  // 无需执行任何操作;我们只是想确保

                  // 我们继续处理列表中的下一个对象

            }

      }

}

public  void  tearDown()  {

      Iterator  i  =  testObjects.iterator();

      while  (i.hasNext())  {

            try  {

                  deleteObject(i.next());

            }  catch  (RuntimeException  e)  {

                  //  Nothing  to  do;  we  just  want  to  make  sure

                  //  we  continue  on  to  the  next  object  in  the  list

            }

      }

}

 

现在我们的测试如下所示:

Now our test looks like this:

public void testAddItemQuantity_severalQuantity_v8(){

      地址 billingAddress = null;

      地址 shippingAddress = null;

      客户 customer = null;

      产品 product = null;

      发票 invoice = null;

      // 设置固定装置

      billingAddress = new Address("1222 1st St SW", "Calgary",

                                   "Alberta", "T2N 2V2", "Canada");

      registerTestObject(billingAddress);

      shippingAddress = new Address("1333 1st St SW", "Calgary",

                                        "Alberta","T2N 2V2", "Canada");

      registerTestObject(shippingAddress);

      customer = new Customer(99, "John", "Doe",

                                                   new BigDecimal("30"),

                                                   billingAddress,

                                                   shippingAddress);

      registerTestObject(product);

      product = new Product(88, "SomeWidget",

                                              new BigDecimal("19.99"));

      registerTestObject(invoice);

      invoice = new Invoice(customer);

      registerTestObject(shippingAddress);

      // 练习 SUT

      invoice.addItemQuantity(product, 5);

      // 验证结果

     LineItem expected =

          new LineItem(invoice, product, 5,

                                 new BigDecimal("30"),

                                 new BigDecimal("69.96"));

      assertContainsExactlyOneLineItem(invoice, expected);

}

public  void  testAddItemQuantity_severalQuantity_v8(){

      Address  billingAddress  =  null;

      Address  shippingAddress  =  null;

      Customer  customer  =  null;

      Product  product  =  null;

      Invoice  invoice  =  null;

      //    Set  up  fixture

      billingAddress  =  new  Address("1222  1st  St  SW",  "Calgary",

                                   "Alberta",  "T2N  2V2",  "Canada");

      registerTestObject(billingAddress);

      shippingAddress  =  new  Address("1333  1st  St  SW",  "Calgary",

                                        "Alberta","T2N  2V2",       "Canada");

      registerTestObject(shippingAddress);

      customer  =  new  Customer(99,  "John",  "Doe",

                                                   new  BigDecimal("30"),

                                                   billingAddress,

                                                   shippingAddress);

      registerTestObject(product);

      product  =  new  Product(88,  "SomeWidget",

                                              new  BigDecimal("19.99"));

      registerTestObject(invoice);

      invoice  =  new  Invoice(customer);

      registerTestObject(shippingAddress);

      //  Exercise  SUT

      invoice.addItemQuantity(product,  5);

      //  Verify  outcome

     LineItem  expected  =

          new  LineItem(invoice,  product,  5,

                                 new  BigDecimal("30"),

                                 new  BigDecimal("69.96"));

      assertContainsExactlyOneLineItem(invoice,  expected);

}

 

我们已经能够删除该try/finally块,除了对的额外调用外registerTestObject,我们的代码要简单得多。但我们仍然可以进一步清理这段代码。例如,为什么我们需要声明变量并将它们初始化为空,然后稍后再重新初始化它们?原始测试需要执行此操作,因为它们必须在finally块中可访问;现在我们已经删除了这个块,我们可以将声明与初始化结合起来:

We have been able to remove the try/finally block and, except for the additional calls to registerTestObject, our code is much simpler. But we can still clean this code up a bit more. Why, for example, do we need to declare the variables and initialize them to null, only to reinitialize them later? This action was needed with the original test because they had to be accessible in the finally block; now that we have removed this block, we can combine the declaration with the initialization:

public void testAddItemQuantity_severalQuantity_v9(){

      // 设置夹具

      Address billingAddress = new Address("1222 1st St SW",

                          "Calgary", "Alberta", "T2N 2V2", "Canada");

      registerTestObject(billingAddress);

      Address shippingAddress = new Address("1333 1st St SW",

                          "Calgary", "Alberta", "T2N 2V2", "Canada");

      registerTestObject(shippingAddress);

      Customer customer = new Customer(99, "John", "Doe",

                                                                    new BigDecimal("30"),

                                                                    billingAddress,

                                                                    shippingAddress);

      registerTestObject(shippingAddress);

      Product product = new Product(88, "SomeWidget",

                                                             new BigDecimal("19.99"));

      registerTestObject(shippingAddress);

      Invoice invoice = new Invoice(customer);

      registerTestObject(shippingAddress);

      // 练习 SUT

      invoice.addItemQuantity(product, 5);

      // 验证结果

     LineItem expected =

          new LineItem(invoice, product, 5,

                                 new BigDecimal("30"),

                                 new BigDecimal("69.95"));

      assertContainsExactlyOneLineItem(invoice, expected);

}

public  void  testAddItemQuantity_severalQuantity_v9(){

      //      Set  up  fixture

      Address  billingAddress  =  new  Address("1222  1st  St  SW",

                          "Calgary",  "Alberta",  "T2N  2V2",  "Canada");

      registerTestObject(billingAddress);

      Address  shippingAddress  =  new  Address("1333  1st  St  SW",

                          "Calgary",  "Alberta",  "T2N  2V2",  "Canada");

      registerTestObject(shippingAddress);

      Customer  customer  =  new  Customer(99,  "John",  "Doe",

                                                                    new  BigDecimal("30"),

                                                                    billingAddress,

                                                                    shippingAddress);

      registerTestObject(shippingAddress);

      Product  product  =  new  Product(88,  "SomeWidget",

                                                             new  BigDecimal("19.99"));

      registerTestObject(shippingAddress);

      Invoice  invoice  =  new  Invoice(customer);

      registerTestObject(shippingAddress);

      //  Exercise  SUT

      invoice.addItemQuantity(product,  5);

      //  Verify  outcome

     LineItem  expected  =

          new  LineItem(invoice,  product,  5,

                                 new  BigDecimal("30"),

                                 new  BigDecimal("69.95"));

      assertContainsExactlyOneLineItem(invoice,  expected);

}

 

清理夹具设置

Cleaning Up the Fixture Setup

现在我们已经清理了断言和夹具拆卸,让我们将注意力转向夹具设置。一个明显的“快速修复”是将每个调用都带到构造函数中,将后续调用带到registerTestObject,并使用提取方法重构来定义创建方法(第 415页)。这将使测试更易于读写。使用创建方法还有另一个优点:它们封装了 SUT 的 API,并通过允许我们只修改一个地方而不必更改每个测试来减少各种对象构造函数更改时的测试维护工作量。

Now that we have cleaned up the assertions and the fixture teardown, let's turn our attention to the fixture setup. One obvious "quick fix" would be to take each of the calls to a constructor, take the subsequent call to registerTestObject, and use an Extract Method refactoring to define a Creation Method (page 415). This will make the test a bit simpler to read and write. The use of Creation Methods has another advantage: They encapsulate the API of the SUT and reduce the test maintenance effort when the various object constructors change by allowing us to modify only a single place rather than having to change each test.

public void testAddItemQuantity_severalQuantity_v10(){

      // 设置夹具

      Address billingAddress =

            createAddress( "1222 1st St SW", "Calgary", "Alberta",

                                      "T2N 2V2", "Canada");

      Address shippingAddress =

            createAddress( "1333 1st St SW", "Calgary", "Alberta",

                                      "T2N 2V2", "Canada");

      Customer customer =

            createCustomer( 99, "John", "Doe", new BigDecimal("30"),

                                        billingAddress, shippingAddress);

      Product product =

            createProduct( 88,"SomeWidget",new BigDecimal("19.99"));

      Invoice invoice = createInvoice(customer);

      // 练习 SUT

      invoice.addItemQuantity(product, 5);

      // 验证结果

     LineItem expected =

          new LineItem(invoice, product,5, new BigDecimal("30"),

                                 new BigDecimal("69.96"));

      assertContainsExactlyOneLineItem(invoice, expected);

}

public  void  testAddItemQuantity_severalQuantity_v10(){

      //  Set  up  fixture

      Address  billingAddress  =

            createAddress(  "1222  1st  St  SW",  "Calgary",  "Alberta",

                                      "T2N  2V2",  "Canada");

      Address  shippingAddress  =

            createAddress(  "1333  1st  St  SW",  "Calgary",  "Alberta",

                                      "T2N  2V2",  "Canada");

      Customer  customer  =

            createCustomer(  99,  "John",  "Doe",  new  BigDecimal("30"),

                                        billingAddress,  shippingAddress);

      Product  product  =

            createProduct(  88,"SomeWidget",new  BigDecimal("19.99"));

      Invoice  invoice  =  createInvoice(customer);

      //  Exercise  SUT

      invoice.addItemQuantity(product,  5);

      //  Verify  outcome

     LineItem  expected  =

          new  LineItem(invoice,  product,5,  new  BigDecimal("30"),

                                 new  BigDecimal("69.96"));

      assertContainsExactlyOneLineItem(invoice,  expected);

}

 

这种夹具设置逻辑仍然存在几个问题。第一个问题是,很难分辨夹具与测试的预期结果有何关系。客户的详细信息是否会以某种方式影响结果?客户的地址会影响结果吗?这个测试到底在验证什么?

This fixture setup logic still suffers from several problems. The first problem is that it is difficult to tell how the fixture is related to the expected outcome of the test. Do the customer's particulars affect the outcome in some way? Does the customer's address affect the outcome? What is this test really verifying?

另一个问题是,此测试展示了硬编码测试数据(请参阅模糊测试)。鉴于我们的 SUT 将我们在数据库中创建的所有对象持久化,如果客户、产品或发票的任何字段必须是唯一的,则使用硬编码测试数据可能会导致不可重复的测试交互测试测试运行战争(请参阅不稳定的测试)。

The other problem is that this test exhibits Hard-Coded Test Data (see Obscure Test). Given that our SUT persists all objects we create in a database, the use of Hard-Coded Test Data may result in an Unrepeatable Test, an Interacting Test, or a Test Run War (see Erratic Test) if any of the fields of the customer, product, or invoice must be unique.

我们可以通过为每个测试生成一个唯一值,然后使用该值来播种我们为测试创建的对象的属性来解决此问题。这种方法将确保每次运行测试时都会创建不同的对象。因为我们已经将对象创建逻辑移到了Creation Methods中,所以这一步相对容易;我们只需将此逻辑放入Creation Method并删除相应的参数即可。这是 Extract Method 重构的另一个应用,在其中我们创建了Creation Method的新、无参数版本。

We can solve this problem by generating a unique value for each test and then using that value to seed the attributes of the objects we create for the test. This approach will ensure that the test creates different objects each time the test is run. Because we have already moved the object creation logic into Creation Methods, this step is relatively easy; we just put this logic into the Creation Method and remove the corresponding parameters. This is another application of the Extract Method refactoring, in which we create a new, parameterless version of the Creation Method.

public void testAddItemQuantity_severalQuantity_v11(){

      final int QUANTITY = 5;

      // 设置夹具

      Address billingAddress = createAnAddress();

      Address shippingAddress = createAnAddress();

      Customer customer = createACustomer(new BigDecimal("30"),

                        billingAddress, shippingAddress);

      Product product = createAProduct(new BigDecimal("19.99"));

      Invoice invoice = createInvoice(customer);

      // 练习 SUT

      invoice.addItemQuantity(product, QUANTITY);

      // 验证结果

     LineItem expected =

          new LineItem(invoice, product, 5, new BigDecimal("30"),

                                  new BigDecimal("69.96"));

      assertContainsExactlyOneLineItem(invoice, expected);

}

private Product createAProduct(BigDecimal unitPrice) {

      BigDecimal uniqueId = getUniqueNumber();

      String uniqueString = uniqueId.toString();

      返回新产品(uniqueId.toBigInteger().intValue(),

                                       uniqueString,unitPrice);

}

public  void  testAddItemQuantity_severalQuantity_v11(){

      final  int  QUANTITY  =  5;

      //        Set  up  fixture

      Address  billingAddress  =  createAnAddress();

      Address  shippingAddress  =  createAnAddress();

      Customer  customer  =  createACustomer(new  BigDecimal("30"),

                        billingAddress,  shippingAddress);

      Product  product  =  createAProduct(new  BigDecimal("19.99"));

      Invoice  invoice  =  createInvoice(customer);

      //  Exercise  SUT

      invoice.addItemQuantity(product,  QUANTITY);

      //  Verify  outcome

     LineItem  expected  =

          new  LineItem(invoice,  product,  5,  new  BigDecimal("30"),

                                  new  BigDecimal("69.96"));

      assertContainsExactlyOneLineItem(invoice,  expected);

}

private  Product  createAProduct(BigDecimal  unitPrice)  {

      BigDecimal  uniqueId  =  getUniqueNumber();

      String  uniqueString  =  uniqueId.toString();

      return  new  Product(uniqueId.toBigInteger().intValue(),

                                       uniqueString,  unitPrice);

}

 

我们将此模式称为匿名创建方法(请参阅创建方法),因为我们声明我们不关心对象的细节。如果 SUT 的预期行为取决于特定值,我们可以将该值作为参数传递,也可以在创建方法的名称中暗示它。

We call this pattern an Anonymous Creation Method (see Creation Method) because we are declaring that we don't care about the particulars of the object. If the expected behavior of the SUT depends on a particular value, we can either pass the value as a parameter or imply it in the name of the creation method.

这个测试现在看起来好多了,但我们还没有完成。预期结果是否以任何方式取决于客户的地址?如果不是,我们可以通过使用提取方法重构(再次!)来完全隐藏它们的构造,以创建一个createACustomer为我们制作它们的方法版本。

This test looks a lot better now, but we are not done yet. Does the expected outcome depend in any way on the addresses of the customer? If not, we can hide their construction completely by using an Extract Method refactoring (again!) to create a version of the createACustomer method that fabricates them for us.

public void testAddItemQuantity_severalQuantity_v12(){

      // 设置夹具

      Customer cust = createACustomer(new BigDecimal("30"));

      Product prod = createAProduct(new BigDecimal("19.99"));

      Invoice invoice = createInvoice(cust);

      // 练习 SUT

      invoice.addItemQuantity(prod, 5);

      // 验证结果

     LineItem expected = new LineItem(invoice, prod, 5,

               new BigDecimal("30"), new BigDecimal("69.96"));

      assertContainsExactlyOneLineItem(invoice, expected);

}

public  void  testAddItemQuantity_severalQuantity_v12(){

      //    Set  up  fixture

      Customer  cust  =  createACustomer(new  BigDecimal("30"));

      Product  prod  =  createAProduct(new  BigDecimal("19.99"));

      Invoice  invoice  =  createInvoice(cust);

      //  Exercise  SUT

      invoice.addItemQuantity(prod,  5);

      //  Verify  outcome

     LineItem  expected  =  new  LineItem(invoice,  prod,  5,

               new  BigDecimal("30"),  new  BigDecimal("69.96"));

      assertContainsExactlyOneLineItem(invoice,  expected);

}

 

通过将创建地址的调用移到创建客户的方法中,我们明确表明地址不会影响我们在此测试中验证的逻辑。但是,结果确实取决于客户的折扣,因此我们将折扣百分比传递给客户创建方法。

By moving the calls that create the addresses into the method that creates the customer, we have made it clear that the addresses do not affect the logic that we are verifying in this test. The outcome does depend on the customer's discount, however, so we pass the discount percentage to the customer creation method.

我们仍有一两件事情需要清理。例如,单价、数量和客户折扣的硬编码测试数据LineItem在测试中重复了两次。我们可以使用用符号常量替换魔力数字 [Fowler] 重构为它们赋予描述角色的名称,从而阐明这些数字的含义。另外,我们用来创建 的构造函数在 SUT 本身的任何地方都没有使用,因为LineItem通常在构造时计算extendedCost。我们应该将这个测试特定的代码转换为测试工具中实现的外部方法 [Fowler]。我们已经看到了如何使用 和 来做到这一点的例子CustomerProduct我们使用参数化创建方法(参见创建方法LineItem)仅根据感兴趣的值返回预期值。

We still have one or two things to clean up. For example, the Hard-Coded Test Data for the unit price, quantity, and customer's discount is repeated twice in the test. We can clarify the meaning of these numbers by using a Replace Magic Number with Symbolic Constant [Fowler] refactoring to give them role-describing names. Also, the constructor we are using to create the LineItem is not used anywhere in the SUT itself because the LineItem normally calculates the extendedCost when it is constructed. We should turn this test-specific code into a Foreign Method [Fowler] implemented within the test harness. We have already seen examples of how to do so with the Customer and Product: We use a Parameterized Creation Method (see Creation Method) to return the expected LineItem based on only those values of interest.

public void testAddItemQuantity_severalQuantity_v13(){

      final int QUANTITY = 5;

      final BigDecimal UNIT_PRICE = new BigDecimal("19.99");

      final BigDecimal CUST_DISCOUNT_PC = new BigDecimal("30");

      // 设置夹具

      Customer customer = createACustomer(CUST_DISCOUNT_PC);

      Product product = createAProduct( UNIT_PRICE);

      Invoice invoice = createInvoice(customer);

      // 练习 SUT

      invoice.addItemQuantity(product, QUANTITY);

      // 验证结果

      final BigDecimal EXTENDED_PRICE = new BigDecimal("69.96");

     LineItem expected =

          new LineItem(invoice, product, QUANTITY,

                                 CUST_DISCOUNT_PC, EXTENDED_PRICE);

      assertContainsExactlyOneLineItem(invoice, expected);

}

public  void  testAddItemQuantity_severalQuantity_v13(){

      final  int  QUANTITY  =  5;

      final  BigDecimal  UNIT_PRICE  =  new  BigDecimal("19.99");

      final  BigDecimal  CUST_DISCOUNT_PC  =  new  BigDecimal("30");

      //    Set  up  fixture

      Customer  customer  =  createACustomer(CUST_DISCOUNT_PC);

      Product  product  =  createAProduct(  UNIT_PRICE);

      Invoice  invoice  =  createInvoice(customer);

      //  Exercise  SUT

      invoice.addItemQuantity(product,  QUANTITY);

      //  Verify  outcome

      final  BigDecimal  EXTENDED_PRICE  =  new  BigDecimal("69.96");

     LineItem  expected  =

          new  LineItem(invoice,  product,  QUANTITY,

                                 CUST_DISCOUNT_PC,  EXTENDED_PRICE);

      assertContainsExactlyOneLineItem(invoice,  expected);

}

 

最后一点:值“69.96”从何而来?如果该值来自某个参考系统的输出,我们应该这样说。因为它只是手动计算并输入到测试中,所以我们可以在测试中显示计算结果,以方便测试读者

One final point: Where did the value "69.96" come from? If this value comes from the output of some reference system, we should say so. Because it was just manually calculated and typed into the test, we can show the calculation in the test for the test reader's benefit.

清理测试

The Cleaned-Up Test

以下是最终的测试清理版本:

Here is the final cleaned-up version of the test:

public void testAddItemQuantity_severalQuantity_v14(){

      final int QUANTITY = 5;

      final BigDecimal UNIT_PRICE = new BigDecimal("19.99");

      final BigDecimal CUST_DISCOUNT_PC = new BigDecimal("30");

      // 设置固定装置

      Customer customer = createACustomer(CUST_DISCOUNT_PC);

      Product product = createAProduct( UNIT_PRICE);

      Invoice invoice = createInvoice(customer);

      // 练习 SUT

      invoice.addItemQuantity(product, QUANTITY);

      // 验证结果

      final BigDecimal BASE_PRICE =

           UNIT_PRICE.multiply(new BigDecimal(QUANTITY));

      final BigDecimal EXTENDED_PRICE =

           BASE_PRICE.subtract(BASE_PRICE.multiply(

                          CUST_DISCOUNT_PC.movePointLeft(2)));

     LineItem 预期 =

          createLineItem(QUANTITY, CUST_DISCOUNT_PC,

                                   EXTENDED_PRICE, 产品, 发票);

      assertContainsExactlyOneLineItem(发票, 预期);

}

public  void  testAddItemQuantity_severalQuantity_v14(){

      final  int  QUANTITY  =  5;

      final  BigDecimal  UNIT_PRICE  =  new  BigDecimal("19.99");

      final  BigDecimal  CUST_DISCOUNT_PC  =  new  BigDecimal("30");

      //    Set  up  fixture

      Customer  customer  =  createACustomer(CUST_DISCOUNT_PC);

      Product  product  =  createAProduct(  UNIT_PRICE);

      Invoice  invoice  =  createInvoice(customer);

      //  Exercise  SUT

      invoice.addItemQuantity(product,  QUANTITY);

      //  Verify  outcome

      final  BigDecimal  BASE_PRICE  =

           UNIT_PRICE.multiply(new  BigDecimal(QUANTITY));

      final  BigDecimal  EXTENDED_PRICE  =

           BASE_PRICE.subtract(BASE_PRICE.multiply(

                          CUST_DISCOUNT_PC.movePointLeft(2)));

     LineItem  expected  =

          createLineItem(QUANTITY,  CUST_DISCOUNT_PC,

                                   EXTENDED_PRICE,  product,  invoice);

      assertContainsExactlyOneLineItem(invoice,  expected);

}

 

我们使用了引入解释变量 [Fowler] 重构来更好地记录BASE_PRICE(价格*数量)和EXTENDED_PRICE(折扣后价格)的计算。修订后的测试现在比我们开始时使用的庞大代码小得多,也更清晰。它很好地履行了测试作为文档的作用(参见第23页)。那么我们发现这个测试验证了什么?它确认添加到发票中的行项目确实已添加到发票中,并且扩展成本基于产品价格、客户折扣和订购数量。

We have used an Introduce Explaining Variable [Fowler] refactoring to better document the calculation of the BASE_PRICE (price*quantity) and EXTENDED_PRICE (the price with discount). The revised test is now much smaller and clearer than the bulky code we started with. It fulfills the role of Tests as Documentation (see page 23) very well. So what did we discover that this test verifies? It confirms that the line items added to an invoice are, indeed, added to the invoice and that the extended cost is based on the product price, the customer's discount, and the quantity ordered.

编写更多测试

Writing More Tests

看来我们花了很多精力来重构这个测试,让它更清晰。我们需要在每个测试上都花这么多精力吗?

It seemed like we went to a lot of effort to refactor this test to make it clearer. Will we have to spend so much effort on every test?

我希望不是!这里的大部分努力都与发现编写测试所需的测试实用程序方法(第599页)有关。我们定义了一种高级语言(参见第41页)来测试我们的应用程序。一旦我们有了这些方法,编写其他测试就变得简单多了。例如,如果我们想编写一个测试来验证当我们更改 a 的数量时是否重新计算了扩展成本LineItem,我们可以重用大多数测试实用程序方法

I should hope not! Much of the effort here related to the discovery of which Test Utility Methods (page 599) were required for writing the test. We defined a Higher-Level Language (see page 41) for testing our application. Once we have those methods in place, writing other tests becomes much simpler. For example, if we want to write a test that verifies that the extended cost is recalculated when we change the quantity of a LineItem, we can reuse most of the Test Utility Methods.

public void testAddLineItem_quantityOne(){

      final BigDecimal BASE_PRICE = UNIT_PRICE;

      final BigDecimal EXTENDED_PRICE = BASE_PRICE;

      // 设置固定装置

      Customer customer = createACustomer(NO_CUST_DISCOUNT);

      Invoice invoice = createInvoice(customer);

      // 练习 SUT

      invoice.addItemQuantity(PRODUCT, QUAN_ONE);

      // 验证结果

     LineItem expected =

          createLineItem( QUAN_ONE, NO_CUST_DISCOUNT,

                                     EXTENDED_PRICE, PRODUCT, invoice);

      assertContainsExactlyOneLineItem( invoice, expected );

}



public void testChangeQuantity_severalQuantity(){

      final int ORIGINAL_QUANTITY = 3;

      final int NEW_QUANTITY = 5;

      final BigDecimal BASE_PRICE =

           UNIT_PRICE.multiply( new BigDecimal(NEW_QUANTITY));

      final BigDecimal EXTENDED_PRICE =

           BASE_PRICE.subtract(BASE_PRICE.multiply(

                                   CUST_DISCOUNT_PC.movePointLeft(2)));

      // 设置夹具

      Customer customer = createACustomer(CUST_DISCOUNT_PC);

      Invoice invoice = createInvoice(customer);

      Product product = createAProduct( UNIT_PRICE);

      invoice.addItemQuantity(product, ORIGINAL_QUANTITY);

      // 练习 SUT

      invoice.changeQuantityForProduct(product, NEW_QUANTITY);

      // 验证结果

     LineItem expected = createLineItem( NEW_QUANTITY,

          CUST_DISCOUNT_PC, EXTENDED_PRICE, PRODUCT, invoice);

      assertContainsExactlyOneLineItem( invoice, expected );

}

public  void  testAddLineItem_quantityOne(){

      final  BigDecimal  BASE_PRICE  =  UNIT_PRICE;

      final  BigDecimal  EXTENDED_PRICE  =  BASE_PRICE;

      //      Set  up  fixture

      Customer  customer  =  createACustomer(NO_CUST_DISCOUNT);

      Invoice  invoice  =  createInvoice(customer);

      //      Exercise  SUT

      invoice.addItemQuantity(PRODUCT,  QUAN_ONE);

      //  Verify  outcome

     LineItem  expected  =

          createLineItem(  QUAN_ONE,  NO_CUST_DISCOUNT,

                                     EXTENDED_PRICE,  PRODUCT,  invoice);

      assertContainsExactlyOneLineItem(  invoice,  expected  );

}



public  void  testChangeQuantity_severalQuantity(){

      final  int  ORIGINAL_QUANTITY  =  3;

      final  int  NEW_QUANTITY  =  5;

      final  BigDecimal  BASE_PRICE  =

           UNIT_PRICE.multiply(  new  BigDecimal(NEW_QUANTITY));

      final  BigDecimal  EXTENDED_PRICE  =

           BASE_PRICE.subtract(BASE_PRICE.multiply(

                                   CUST_DISCOUNT_PC.movePointLeft(2)));

      //        Set  up  fixture

      Customer  customer  =  createACustomer(CUST_DISCOUNT_PC);

      Invoice  invoice  =  createInvoice(customer);

      Product  product  =  createAProduct(  UNIT_PRICE);

      invoice.addItemQuantity(product,  ORIGINAL_QUANTITY);

      //  Exercise  SUT

      invoice.changeQuantityForProduct(product,  NEW_QUANTITY);

      //  Verify  outcome

     LineItem  expected  =  createLineItem(  NEW_QUANTITY,

          CUST_DISCOUNT_PC,  EXTENDED_PRICE,  PRODUCT,  invoice);

      assertContainsExactlyOneLineItem(  invoice,  expected  );

}

 

这个测试大约花了两分钟编写完成,并且不需要添加任何新的测试实用方法。相比之下,以原始风格编写一个全新的测试需要花费很长时间。编写测试所节省的工作量只是其中的一部分 — 我们还需要考虑每次需要重新访问现有测试时节省的理解工作量。在开发项目和后续维护活动的过程中,这些成本节省将真正累积起来。

This test was written in about two minutes and did not require adding any new Test Utility Methods. Contrast that with how long it would have taken to write a completely new test in the original style. And the effort saved in writing the tests is just part of the equation—we also need to consider the effort we saved understanding existing tests each time we need to revisit them. Over the course of a development project and the subsequent maintenance activity, this cost savings will really add up.

进一步压实

Further Compaction

编写这些额外的测试揭示了更多的测试代码重复来源(第 213页)。例如,我们似乎总是创建 aCustomer和 an Invoice。为什么不合并这两行呢?同样,我们在测试方法中不断定义和初始化QUANTITYCUSTOMER_DISCOUNT_PC常量。为什么我们不能只做一次这些任务?Product在这些测试中似乎不起任何作用;我们总是以完全相同的方式创建它。我们能把这个责任也分解出来吗?当然!我们只需对每组重复代码应用提取方法重构,即可创建更强大的创建方法

Writing these additional tests revealed a few more sources of Test Code Duplication (page 213). For example, it seems that we always create both a Customer and an Invoice. Why not combine these two lines? Similarly, we continually define and initialize the QUANTITY and CUSTOMER_DISCOUNT_PC constants inside our test methods. Why can't we do these tasks just once? The Product does not seem to play any roles in these tests; we always create it exactly the same way. Can we factor this responsibility out, too? Certainly! We just apply an Extract Method refactoring to each set of duplicated code to create more powerful Creation Methods.

public void testAddItemQuantity_severalQuantity_v15(){

      // 设置夹具

      Invoice invoice = createCustomerInvoice(CUST_DISCOUNT_PC);

      // 练习 SUT

      invoice.addItemQuantity(PRODUCT, SEVERAL);

      // 验证结果

      final BigDecimal BASE_PRICE =

           UNIT_PRICE.multiply(new BigDecimal(SEVERAL));

      final BigDecimal EXTENDED_PRICE =

           BASE_PRICE.subtract(BASE_PRICE.multiply(

                         CUST_DISCOUNT_PC.movePointLeft(2)));

     LineItem expected = createLineItem( SEVERAL,

          CUST_DISCOUNT_PC, EXTENDED_PRICE, PRODUCT, invoice);

      assertContainsExactlyOneLineItem(invoice, expected);

}



public void testAddLineItem_quantityOne_v2(){

      final BigDecimal BASE_PRICE = UNIT_PRICE;

      final BigDecimal EXTENDED_PRICE = BASE_PRICE;

      // 设置夹具

      Invoice invoice = createCustomerInvoice(NO_CUST_DISCOUNT);

      // 练习 SUT

      invoice.addItemQuantity(PRODUCT, QUAN_ONE);

      // 验证结果

     LineItem expected = createLineItem( SEVERAL,

             CUST_DISCOUNT_PC, EXTENDED_PRICE, PRODUCT, invoice);

      assertContainsExactlyOneLineItem( invoice, expected );

}



public void testChangeQuantity_severalQuantity_V2(){

      final int NEW_QUANTITY = SEVERAL + 2;

      final BigDecimal BASE_PRICE =

           UNIT_PRICE.multiply( new BigDecimal(NEW_QUANTITY));

      final BigDecimal EXTENDED_PRICE =

           BASE_PRICE.subtract(BASE_PRICE.multiply(

                                    CUST_DISCOUNT_PC.movePointLeft(2)));

      // 设置夹具

      发票发票 = createCustomerInvoice(CUST_DISCOUNT_PC);

      发票.addItemQuantity(PRODUCT, SEVERAL);

      // 练习 SUT

      发票.changeQuantityForProduct(PRODUCT, NEW_QUANTITY);

      // 验证结果

     LineItem expected = createLineItem( NEW_QUANTITY,

           CUST_DISCOUNT_PC, EXTENDED_PRICE, PRODUCT, 发票);

      assertContainsExactlyOneLineItem( 发票, 预期 );

}

public  void  testAddItemQuantity_severalQuantity_v15(){

      //    Set  up  fixture

      Invoice  invoice  =  createCustomerInvoice(CUST_DISCOUNT_PC);

      //  Exercise  SUT

      invoice.addItemQuantity(PRODUCT,  SEVERAL);

      //  Verify  outcome

      final  BigDecimal  BASE_PRICE  =

           UNIT_PRICE.multiply(new  BigDecimal(SEVERAL));

      final  BigDecimal  EXTENDED_PRICE  =

           BASE_PRICE.subtract(BASE_PRICE.multiply(

                         CUST_DISCOUNT_PC.movePointLeft(2)));

     LineItem  expected  =  createLineItem(  SEVERAL,

          CUST_DISCOUNT_PC,  EXTENDED_PRICE,  PRODUCT,  invoice);

      assertContainsExactlyOneLineItem(invoice,  expected);

}



public  void  testAddLineItem_quantityOne_v2(){

      final  BigDecimal  BASE_PRICE  =  UNIT_PRICE;

      final  BigDecimal  EXTENDED_PRICE  =  BASE_PRICE;

      //    Set  up  fixture

      Invoice  invoice  =  createCustomerInvoice(NO_CUST_DISCOUNT);

      //      Exercise  SUT

      invoice.addItemQuantity(PRODUCT,  QUAN_ONE);

      //  Verify  outcome

     LineItem  expected  =  createLineItem(  SEVERAL,

             CUST_DISCOUNT_PC,  EXTENDED_PRICE,  PRODUCT,  invoice);

      assertContainsExactlyOneLineItem(  invoice,  expected  );

}



public  void  testChangeQuantity_severalQuantity_V2(){

      final  int  NEW_QUANTITY  =  SEVERAL  +  2;

      final  BigDecimal  BASE_PRICE  =

           UNIT_PRICE.multiply(  new  BigDecimal(NEW_QUANTITY));

      final  BigDecimal  EXTENDED_PRICE  =

           BASE_PRICE.subtract(BASE_PRICE.multiply(

                                    CUST_DISCOUNT_PC.movePointLeft(2)));

      //        Set  up  fixture

      Invoice  invoice  =  createCustomerInvoice(CUST_DISCOUNT_PC);

      invoice.addItemQuantity(PRODUCT,  SEVERAL);

      //  Exercise  SUT

      invoice.changeQuantityForProduct(PRODUCT,  NEW_QUANTITY);

      //  Verify  outcome

     LineItem  expected  =  createLineItem(  NEW_QUANTITY,

           CUST_DISCOUNT_PC,  EXTENDED_PRICE,  PRODUCT,  invoice);

      assertContainsExactlyOneLineItem(  invoice,  expected  );

}

 

现在,我们将需要理解的代码行数从原始测试中的 35 条语句减少到仅 6 条语句。4我们只需维护原始代码的六分之一多一点!我们可以更进一步,将夹具设置分解为一个setUp方法,但只有大量测试需要相同的 Customer/Discount/Invoice 配置时,这种努力才值得。如果我们想从其他测试用例类(第 373页) 重用这些测试实用程序方法,我们可以使用提取超类 [Fowler] 重构来创建测试用例超类(第 638页),然后使用上拉方法 [Fowler] 重构将测试实用程序方法移到其中,以便可以重用它们。

We have now reduced the number of lines of code we need to understand from 35 statements in the original test to just 6 statements.4 We are left with just a bit more than one sixth of the original code to maintain! We could go further by factoring out the fixture setup into a setUp method, but that effort would be worthwhile only if a lot of tests needed the same Customer/Discount/Invoice configuration. If we wanted to reuse these Test Utility Methods from other Testcase Classes (page 373), we could use an Extract Superclass [Fowler] refactoring to create a Testcase Superclass (page 638), and then use a Pull Up Method [Fowler] refactoring to move the Test Utility Methods to it so they can be reused.

第一部分

叙述

Part I

The Narratives

 

第 1 章

简要介绍

Chapter 1

A Brief Tour

 

关于本章

About This Chapter

本书中有很多原则、模式和气味,甚至还有更多无法在书中展示的模式。你需要全部学习它们吗?你需要全部使用它们吗?可能不需要!本章简要介绍了整本书的大部分内容。在深入研究感兴趣的特定模式或气味之前,你可以将其用作材料的快速浏览。在探索更详细的叙述章节之前,你也可以将其用作热身。

There are a lot of principles, patterns, and smells in this book—and even more patterns that couldn't fit into the book. Do you need to learn them all? Do you need to use them all? Probably not! This chapter provides an abbreviated introduction to the bulk of the material in the entire book. You can use it as a quick tour of the material before diving into particular patterns or smells of interest. You can also use it as a warm-up before exploring the more detailed narrative chapters.

最简单的可能有效的测试自动化策略

The Simplest Test Automation Strategy That Could Possibly Work

有一种适用于许多项目的简单测试自动化策略。本节介绍这种最小测试策略。这里引用的原则、模式和味道是长期对我们有益的核心模式。如果我们学会有效地应用它们,我们很可能会在测试自动化工作中取得成功。如果我们发现使用这些模式确实无法使最小测试策略在我们的项目中发挥作用,我们可以回到这些模式的完整描述和其他叙述中列出的替代模式。

There is a simple test automation strategy that will work for many, many projects. This section describes this minimal test strategy. The principles, patterns, and smells referenced here are the core patterns that will serve us well in the long run. If we learn to apply them effectively, we will probably be successful in our test automation endeavors. If we find that we really cannot make the minimal test strategy work on our project by using these patterns, we can fall back to the alternative patterns listed in the full descriptions of these patterns and in the other narratives.

我把这个简单的策略分为五个部分:

I have laid out this simple strategy in five parts:

  • 开发过程:我们用于开发代码的过程如何影响我们的测试。
  • Development Process: How the process we use to develop the code affects our tests.
  • 客户测试:我们应该编写第一个测试作为“完成后是什么样子”的最终定义。
  • Customer Tests: The first tests we should write as the ultimate definition of "what done looks like."
  • 单元测试:帮助我们的设计逐步出现并确保所有代码都经过测试的测试。
  • Unit Tests: The tests that help our design emerge incrementally and ensure that all our code is tested.
  • 可测试性设计:使我们的设计更易于测试的模式,从而降低测试自动化的成本。
  • Design for Testability: The patterns that make our design easier to test, thereby reducing the cost of test automation.
  • 测试组织:如何组织我们的测试方法第 348页)和测试用例类第 373页)。
  • Test Organization: How we can organize our Test Methods (page 348) and Testcase Classes (page 373).

开发过程

Development Process

首先要说的是:我们什么时候编写测试?在编写软件之前编写测试有几个好处。特别是,它让我们对成功的定义有了一致的认识。1

First things first: When do we write our tests? Writing tests before we write our software has several benefits. In particular, it gives us an agreed-upon definition of what success looks like.1

在进行新软件开发时,我们努力通过首先自动执行一套客户测试来验证应用程序提供的功能,从而实现故事测试驱动的开发。为了确保我们的所有软件都经过测试,我们用一套单元测试来增强这些测试,这些测试验证所有代码路径,或者至少验证客户测试未涵盖的所有代码路径。我们可以使用代码覆盖率工具来发现哪些代码未被执行,然后改进单元测试以适应未经测试的代码。2

When doing new software development, we strive to do storytest-driven development by first automating a suite of customer tests that verify the functionality provided by the application. To ensure that all of our software is tested, we augment these tests with a suite of unit tests that verify all code paths or, at a minimum, all the code paths that are not covered by the customer tests. We can use code coverage tools to discover which code is not being exercised and then retrofit unit tests to accommodate the untested code.2

通过将单元测试和客户测试组织到单独的测试套件中,我们确保在必要时可以只运行单元测试或客户测试。单元测试应始终在我们签入之前通过;这就是我们所说的“保持绿色”的意思。为了确保单元测试频繁运行,我们可以将它们包含在作为集成构建 [SCM] 的一部分运行的冒烟测试 [SCM] 中。尽管许多客户测试在构建相应的功能之前都会失败,但将所有通过的客户测试作为集成构建阶段的一部分运行仍然是有用的 - 但前提是此步骤不会减慢构建速度太多。在这种情况下,我们可以将它们排除在签入构建之外,而只是每晚运行它们。

By organizing the unit tests and customer tests into separate test suites, we ensure that we can run just the unit tests or just the customer tests if necessary. The unit tests should always pass before we check them in; this is what we mean by the phrase "keep the bar green." To ensure that the unit tests are run frequently, we can include them in the Smoke Tests [SCM] that are run as part of the Integration Build [SCM]. Although many of the customer tests will fail until the corresponding functionality is built, it is nevertheless useful to run all the passing customer tests as part of the integration build phase—but only if this step does not slow the build down too much. In that case, we can leave them out of the check-in build and simply run them every night.

我们可以通过测试驱动开发 (TDD)来确保我们的软件是可测试的。也就是说,我们在编写代码之前编写单元测试,并使用测试来帮助我们定义软件的设计。此策略有助于将所有需要验证的业务逻辑集中在可以独立于数据库进行测试的定义明确的对象中。虽然我们也应该对数据访问层和数据库进行单元测试,但我们尽量在业务逻辑的单元测试中将对数据库的依赖性降至最低。

We can ensure that our software is testable by doing test-driven development (TDD). That is, we write the unit tests before we write the code, and we use the tests to help us define the software's design. This strategy helps concentrate all the business logic that needs verification in well-defined objects that can be tested independently of the database. Although we should also have unit tests for the data access layer and the database, we try to keep the dependency on the database to a minimum in the unit tests for the business logic.

客户测试

Customer Tests

客户测试应该抓住客户希望系统实现的功能的本质。无论我们是否真正实现测试自动化,在开始开发之前枚举测试都是重要的一步,因为它可以帮助开发团队了解客户真正想要什么;这些测试定义了成功是什么样的。我们可以使用脚本测试(第285页)或数据驱动测试第 288页)来自动化测试,具体取决于谁在准备测试;如果我们使用数据驱动测试,客户可以参与测试自动化。在极少数情况下,我们甚至可能使用记录的测试第 278页)对现有应用程序进行回归测试,同时重构应用程序以提高其可测试性。当然,一旦我们开发出涵盖功能的其他测试,我们通常会丢弃这些测试,因为记录的测试往往是脆弱测试第 239页)。

The customer tests should capture the essence of what the customer wants the system to do. Enumerating the tests before we begin their development is an important step whether or not we actually automate the tests, because it helps the development team understand what the customer really wants; these tests define what success looks like. We can automate the tests using Scripted Tests (page 285) or Data-Driven Tests (page 288) depending on who is preparing the tests; customers can take part in test automation if we use Data-Driven Tests. On rare occasions, we might even use Recorded Tests (page 278) for regression testing an existing application while we refactor the application to improve its testability. Of course, we usually discard these tests once we have developed other tests that cover the functionality, because Recorded Tests tend to be Fragile Tests (page 239).

在开发过程中,我们努力使客户测试能够代表系统的实际使用情况。遗憾的是,这个目标往往与避免测试过长的努力相冲突,因为长测试往往是模糊测试第 186页),并且当它们在测试中途失败时往往不能提供很好的缺陷定位(参见第22页)。我们还可以使用精心编写的测试作为文档(参见第23页)来确定系统应如何工作。为了使测试简单易懂,我们可以通过对一个或多个服务外观[CJ2EEP]执行皮下测试(参见第337页的层测试)来绕过用户界面。服务外观将所有业务逻辑封装在一个简单接口之后,而表示层也会使用该接口。

During their development, we strive to make our customer tests representative of how the system is really used. Unfortunately, this goal often conflicts with attempts to keep the tests from becoming too long, because long tests are often Obscure Tests (page 186) and tend not to provide very good Defect Localization (see page 22) when they fail partway through the test. We can also use well-written Tests as Documentation (see page 23) to identify how the system is supposed to work. To keep the tests simple and easy to understand, we can bypass the user interface by performing Subcutaneous Testing (see Layer Test on page 337) against one or more Service Facades [CJ2EEP]. Service Facades encapsulate all of the business logic behind a simple interface that is also used by the presentation layer.

每个测试都需要一个起点。作为测试计划的一部分,我们确保每次运行测试时,每个测试都设置这个起点,即测试装置。这个新装置(第311页)可确保测试不依赖于它们自己未设置的任何内容,从而帮助我们避免交互测试(请参阅第228页的不稳定测试)。我们避免使用共享装置第 317页),除非它是不可变共享装置,以避免开始滑向不稳定测试的危险。

Every test needs a starting point. As part of our testing plan, we take care that each test sets up this starting point, known as the test fixture, each time the test is run. This Fresh Fixture (page 311) helps us avoid Interacting Tests (see Erratic Test on page 228) by ensuring that tests do not depend on anything they did not set up themselves. We avoid using a Shared Fixture (page 317), unless it is an Immutable Shared Fixture, to avoid starting down the slippery slope to Erratic Tests.

如果我们的应用程序通常与其他应用程序交互,我们可能需要使用某种形式的测试替身第 522页)来将其与开发环境中没有的任何应用程序隔离开来,这些替身充当与其他应用程序的接口。如果测试由于数据库访问或其他慢速组件而运行得太慢,我们可以用功能等效的伪对象第 551页)替换它们以加快测试速度,从而鼓励开发人员更频繁地运行它们。如果可能的话,我们避免使用链式测试(第454页)——它们只是伪装的测试异味交互测试。

If our application normally interacts with other applications, we may need to isolate it from any applications that we do not have in our development environment by using some form of Test Double (page 522) for the objects that act as interfaces to the other applications. If the tests run too slowly because of database access or other slow components, we can replace them with functionally equivalent Fake Objects (page 551) to speed up our tests, thereby encouraging developers to run them more regularly. If at all possible, we avoid using Chained Tests (page 454)—they are just the test smell Interacting Tests in disguise.

单元测试

Unit Tests

为了使单元测试有效,每个单元测试都应该是完全自动化测试第 26页),通过类的公共接口对其进行往返测试。我们可以通过确保每个测试都是单一条件测试(参见第45页)来努力实现缺陷定位,该测试在单一场景中执行单一方法或对象。我们还应该编写测试,以便四阶段测试(第358页)的每个部分都易于识别,这使我们能够将测试用作文档

For our unit tests to be effective, each one should be a Fully Automated Test (page 26) that does a round-trip test against a class through its public interface. We can strive for Defect Localization by ensuring that each test is a Single-Condition Test (see page 45) that exercises a single method or object in a single scenario. We should also write our tests so that each part of the Four-Phase Test (page 358) is easily recognizable, which enables us to use the Tests as Documentation.

我们使用Fresh Fixture策略,这样就不必担心交互测试Fixture 拆卸。我们首先为要测试的每个类创建一个测试用例类(请参阅第617页的每个类的测试用例类),每个测试都是该类上的一个单独的测试方法。每个测试方法都可以使用委托设置(第411页)来构建一个最小 Fixture (第302页),通过调用命名良好的创建方法第 415页)来构建每个测试 Fixture 所需的对象,从而使测试易于理解。

We use a Fresh Fixture strategy so that we do not have to worry about Interacting Tests or fixture teardown. We begin by creating a Testcase Class for each class we are testing (see Testcase Class per Class on page 617), with each test being a separate Test Method on that class. Each Test Method can use Delegated Setup (page 411) to build a Minimal Fixture (page 302) that makes the tests easily understood by calling well-named Creation Methods (page 415) to build the objects required for each test fixture.

为了使测试具有自检功能(自检测试;参见第26页),我们将每个测试的预期结果表示为一个或多个预期对象(参见第462页的状态验证),并使用内置的相等性断言(参见第 362页的断言方法)或实现我们自己特定于测试的相等性的自定义断言(第 474页)将它们与被测系统 (SUT)返回的实际对象进行比较。如果预计多个测试将产生相同的结果,我们可以将验证逻辑分解为描述结果的验证方法(参见自定义断言),以便测试读者更容易识别。

To make the tests self-checking (Self-Checking Test; see page 26), we express the expected outcome of each test as one or more Expected Objects (see State Verification on page 462) and compare them with the actual objects returned by the system under test (SUT) using the built-in Equality Assertions (see Assertion Method on page 362) or Custom Assertions (page 474) that implement our own test-specific equality. If several tests are expected to result in the same outcome, we can factor out the verification logic into an outcome-describing Verification Method (see Custom Assertion) that the test reader can more easily recognize.

如果由于无法找到执行代码路径的方法而导致代码未经测试(请参阅第 268页的生产错误),则可以使用测试桩(第529页)来控制SUT 的间接输入。如果由于并非所有系统行为都可通过其公共接口观察到而导致需求未经测试(请参阅生产错误),则可以使用模拟对象第 544页)来拦截和验证SUT 的间接输出。

If we have Untested Code (see Production Bugs on page 268) because we cannot find a way to execute the path through the code, we can use a Test Stub (page 529) to gain control of the indirect inputs of the SUT. If there are Untested Requirements (see Production Bugs) because not all of the system's behavior is observable via its public interface, we can use a Mock Object (page 544) to intercept and verify the indirect outputs of the SUT.

可测试性设计

Design for Testability

如果采用分层架构 [ DDDPEAAWWW ],自动化测试会简单得多。至少,我们应该将业务逻辑与数据库和用户界面分开,这样我们就可以方便地使用皮下测试服务层测试(请参阅层测试)对其进行测试。通过仅使用内存对象进行大部分(如果不是全部)测试,我们可以将对数据库沙箱第 650页)的依赖降至最低。此方案可让运行时环境自动为我们实现垃圾收集拆卸(第 500页),这意味着我们可以避免编写可能复杂、容易出错的拆卸逻辑(资源泄漏的必然来源;请参阅不稳定的测试)。它还可以通过减少磁盘 I/O(比内存操作慢得多)来帮助我们避免“慢测试”第 253页)。

Automated testing is much simpler if we adopt a Layered Architecture [DDD, PEAA, WWW]. At a minimum, we should separate our business logic from the database and the user interface, thereby enabling us to test it easily using either Subcutaneous Tests or Service Layer Tests (see Layer Test). We can minimize any dependence on a Database Sandbox (page 650) by doing most—if not all—of our testing using in-memory objects only. This scheme lets the runtime environment implement Garbage-Collected Teardown (page 500) for us automatically, meaning that we can avoid writing potentially complex, error-prone teardown logic (a sure source of Resource Leakage; see Erratic Test). It also helps us avoid Slow Tests (page 253) by reducing disk I/O, which is much slower than memory manipulation.

如果我们正在构建GUI,我们应尝试将复杂的 GUI 逻辑排除在可视类之外。使用将所有决策委托给非可视类的Humble Dialog(请参阅第695页的Humble Object),我们可以为 GUI 逻辑编写单元测试(例如,启用/禁用按钮),而无需实例化图形对象或它们所依赖的框架。

If we are building a GUI, we should try to keep the complex GUI logic out of the visual classes. Using a Humble Dialog (see Humble Object on page 695) that delegates all decision making to nonvisual classes allows us to write unit tests for the GUI logic (e.g., enabling/disabling buttons) without having to instantiate the graphical objects or the framework on which they depend.

如果应用程序足够复杂,或者我们预计要构建将被其他项目重用的组件,那么我们可以使用组件测试来增强单元测试,以单独验证每个组件的行为。我们可能需要使用测试替身来替换组件所依赖的任何组件。要在运行时安装测试替身,我们可以使用依赖注入第 678页)、依赖查找(第686页)或子类单例(请参阅第 579页的测试特定子类)。

If the application is complex enough or if we are expected to build components that will be reused by other projects, we can augment the unit tests with component tests that verify the behavior of each component in isolation. We will probably need to use Test Doubles to replace any components on which our component depends. To install the Test Doubles at runtime, we can use either Dependency Injection (page 678), Dependency Lookup (page 686), or a Subclassed Singleton (see Test-Specific Subclass on page 579).

测试组织

Test Organization

如果测试用例类中的测试方法过多,我们可以考虑根据测试验证的方法(或特性)或它们的装置需求来拆分类。这些模式分别称为每个特性一个测试用例类(第 624页) 和每个装置一个测试用例类(第 631页)。每个装置一个测试用例类允许我们将所有装置设置代码移到方法中,这种方法称为隐式设置(第 424页)。然后,我们可以将结果测试用例类的测试套件对象(第 387页) 聚合到单个测试套件对象中,从而得到一个套件套件(参见测试套件对象),其中包含原始测试用例类中的所有测试。这个测试套件对象可以依次添加到包含包或命名空间的测试套件对象中。然后,我们可以运行所有测试,或者只运行与我们正在开发的软件领域相关的子集。setUp

If we end up with too many Test Methods on our Testcase Class, we can consider splitting the class based on either the methods (or features) verified by the tests or their fixture needs. These patterns are called Testcase Class per Feature (page 624) and Testcase Class per Fixture (page 631), respectively. Testcase Class per Fixture allows us to move all of the fixture setup code into the setUp method, an approach called Implicit Setup (page 424). We can then aggregate the Test Suite Objects (page 387) for the resulting Testcase Classes into a single Test Suite Object, resulting in a Suite of Suites (see Test Suite Object) containing all the tests from the original Testcase Class. This Test Suite Object can, in turn, be added to the Test Suite Object for the containing package or namespace. We can then run all of the tests or just a subset that is relevant to the area of the software in which we are working.

下一步是什么?

What's Next?

这篇关于最重要的目标、原则、模式和异味的快速介绍只是对测试自动化的简要介绍。第 2 章第 14章对这里涉及的每个领域进行了更详细的概述。如果您已经发现了一些想要进一步了解的模式或异味,您当然可以直接继续阅读第 II 部分第 III部分中的详细描述。否则,您的下一步是深入研究后续叙述,这些叙述对这些模式及其替代方案进行了更深入的检查。首先是第 2 章“测试异味”,其中描述了一些常见的“测试异味”,这些异味激发了我们在测试中进行的大部分重构。

This whirlwind tour of the most important goals, principles, patterns, and smells is just a brief introduction to test automation. Chapters 2 through 14 give a more detailed overview of each area touched upon here. If you have already spotted some patterns or smells you want to learn more about, you can certainly proceed directly to the detailed descriptions in Parts II and III. Otherwise, your next step is to delve into the subsequent narratives, which provide a somewhat more in-depth examination of these patterns and the alternatives to them. First up is Chapter 2, Test Smells, which describes some common "test smells" that motivate much of the refactoring we do on our tests.

第 2 章

测试气味

Chapter 2

Test Smells

 

关于本章

About This Chapter

第 1 章简要介绍”简要介绍了本书中介绍的核心模式和异味。本章更详细地介绍了我们在项目中可能遇到的“测试异味”。我们首先探讨测试异味的基本概念,然后继续研究三大类的异味:测试代码异味、自动化测试行为异味和与自动化测试相关的项目异味。

Chapter 1, A Brief Tour, provided a very quick introduction to the core patterns and smells covered in this book. This chapter provides a more detailed examination of the "test smells" we are likely to encounter on our projects. We explore the basic concept of test smells first, and then move on to investigate the smells in three broad categories: test code smells, automated test behavior smells, and project smells related to automated testing.

测试气味简介

An Introduction to Test Smells

马丁·福勒 (Martin Fowler)在他的著作《重构:改进现有代码的设计》中记录了多种无需实际更改代码功能即可更改代码设计的方法。这种重构的动机是识别面向对象代码中经常出现的“坏味道”。这些代码味道在肯特·贝克 (Kent Beck) 合著的一章中进行了描述,该章以贝克奶奶的名言开头:“如果它很臭,就改变它。”这句话的背景是这样一个问题:“你怎么知道你需要给婴儿换尿布?”因此,一个新术语被添加到了程序员的词典中。

In his book Refactoring: Improving the Design of Existing Code, Martin Fowler documented a number of ways that the design of code can be changed without actually changing what the code does. The motivation for this refactoring was the identification of "bad smells" that frequently occur in object-oriented code. These code smells were described in a chapter coauthored by Kent Beck that started with the famous quote from Grandma Beck: "If it stinks, change it." The context of this quote was the question, "How do you know you need to change a baby's diaper?" And so a new term was added to the programmer's lexicon.

《重构》中描述的代码异味主要针对生产代码中常见的问题。我们中的许多人早就怀疑自动化测试脚本中存在一些独特的异味。在 XP2001 上,论文“重构测试代码” [RTC]通过识别出测试代码中特有的大量“坏味道”证实了这些怀疑。作者还推荐了一组可应用于测试以消除有害气味的重构。

The code smells described in Refactoring focused on problems commonly found in production code. Many of us had long suspected that there were smells unique to automated test scripts. At XP2001, the paper "Refactoring Test Code" [RTC] confirmed these suspicions by identifying a number of "bad smells" that occur specifically in test code. The authors also recommended a set of refactorings that can be applied to the tests to remove the noxious smells.

本章概述了这些测试气味。每个测试气味的更详细示例可在参考部分中找到。

This chapter provides an overview of these test smells. More detailed examples of each test smell can be found in the reference section.

什么是测试气味?

What's a Test Smell?

气味是问题的症状。气味并不一定能告诉我们哪里出了问题,因为某种气味可能来自多个来源。本书中提到的大多数气味都有几种不同名称的原因;有些原因甚至出现在几种气味下。这是因为根本原因可能通过几种不同的症状(即气味)显现出来。

A smell is a symptom of a problem. A smell doesn't necessarily tell us what is wrong, because a particular smell may originate from any of several sources. Most of the smells in this book have several different named causes; some causes even appear under several smells. That's because a root cause may reveal itself through several different symptoms (i.e., smells).

并非所有问题都被视为气味,有些问题甚至可能是几种气味的根本原因。用于确定某物是否真的是气味(而不是仅仅是问题)的“奥卡姆剃刀”测试是“嗅觉测试”。也就是说,气味必须抓住我们的鼻子并告诉我们,“这里有问题。”正如下一节所讨论的,我根据气味表现出的症状类型(它们如何“抓住我们的鼻子”)对气味进行了分类。

Not all problems are considered smells, and some problems may even be the root cause of several smells. The "Occam's razor" test for deciding whether something really is a smell (versus just a problem) is the "sniffability test." That is, the smell must grab us by the nose and say, "Something is wrong here." As discussed in the next section, I have classified the smells based on the kinds of symptoms they exhibit (how they "grab us by the nose").

根据“可嗅性”标准,我将之前论文和文章中列出的一些测试气味降级为“原因”状态。我大部分保留了它们的名称不变,以便我们在谈论应用模式的特定副作用时仍然可以参考它们。在这种情况下,直接提及原因比提及更普遍但可嗅的气味更合适。

Based on the "sniffability" criteria, I have demoted some of the test smells listed in prior papers and articles to "cause" status. I have mostly left their names unchanged so that we can still refer to them when talking about a particular side effect of applying a pattern. In this case, it is more appropriate to refer directly to the cause rather than to the more general but sniffable smell.

测试气味的种类

Kinds of Test Smells

多年来,我们发现至少有两种不同的异味:代码异味,必须在查看代码时识别;行为异味,会影响测试执行时的结果。

Over the years we have discovered that there are at least two different kinds of smells: code smells, which must be recognized when looking at code, and behavior smells, which affect the outcome of tests as they execute.

代码异味是开发人员、测试人员或教练在阅读或编写测试代码时可能会注意到的编码级反模式。也就是说,代码看上去不太对劲,或者不能很清楚地表达其意图。我们必须先识别代码异味,然后才能采取任何措施,而措施的必要性可能对每个人来说并不同样明显。代码异味适用于所有类型的测试,包括脚本测试第 285页)和记录测试第 278页)。当我们必须维护记录的代码时,它们对于记录测试来说尤其重要。不幸的是,大多数记录测试都受到模糊测试(第 186页)的影响,因为它们是由不知道哪些内容与人类读者相关的工具记录的。

Code smells are coding-level anti-patterns that a developer, tester, or coach may notice while reading or writing test code. That is, the code just doesn't look quite right or doesn't communicate its intent very clearly. Code smells must first be recognized before we can take any action, and the need for action may not be equally obvious to everyone. Code smells apply to all kinds of tests, including both Scripted Tests (page 285) and Recorded Tests (page 278). They become particularly relevant for Recorded Tests when we must maintain the recorded code. Unfortunately, most Recorded Tests suffer from Obscure Tests (page 186), because they are recorded by a tool that doesn't know what is relevant to the human reader.

相比之下,行为异味则更难忽略,因为它们会在最不合时宜的时候导致测试失败(或根本无法编译),例如当我们试图将代码集成到关键构建中时;我们被迫在“让测试条变绿”之前发现问题。与代码异味一样,行为异味与脚本测试记录测试都相关。

Behavior smells, by contrast, are much more difficult to ignore because they cause tests to fail (or not compile at all) at the most inopportune times, such as when we are trying to integrate our code into a crucial build; we are forced to unearth the problems before we can "make the bar green." Like code smells, behavior smells are relevant to both Scripted Tests and Recorded Tests.

开发人员通常在自动化、维护和运行测试时会注意到代码和行为异味。最近,我们发现了第三种异味——通常由项目经理或客户注意到的异味,他们不会查看测试代码或运行测试。这些项目异味是项目整体健康状况的指标。

Developers typically notice code and behavior smells when they automate, maintain, and run tests. More recently, we have identified a third kind of smell—a smell that is usually noticed by the project manager or the customer, who does not look at the test code or run the tests. These project smells are indicators of the overall health of a project.

如何处理气味?

What to Do about Smells?

有些气味是不可避免的,因为它们需要花费太多精力才能消除。重要的是,我们要意识到这些气味,并知道它们产生的原因。然后,我们可以有意识地决定必须解决哪些问题,以保持项目高效运行。

Some smells are inevitable simply because they take too much effort to eliminate. The important thing is that we are aware of the smells and know what causes them. We can then make a conscious decision about which ones we must address to keep the project running efficiently.

决定必须消除哪些异味取决于成本和收益之间的平衡。有些异味比其他异味更难消除;有些异味比其他异味更令人痛苦。我们需要消除那些给我们带来最大痛苦的异味,因为它们会阻碍我们取得成功。话虽如此,通过选择合理的测试自动化策略并遵循良好的测试自动化编码标准,可以避免许多异味。

The decision of which smells must be eliminated comes down to the balance between cost and benefit. Some smells are harder to stamp out than others; some smells cause more grief than others. We need to eradicate those smells that cause us the most grief because they will keep us from being successful. That being said, many smells can be avoided by selecting a sound test automation strategy and by following good test automation coding standards.

虽然我们仔细地描述了各种气味,但需要注意的是,我们经常会同时观察到每种气味的症状。例如,项目气味是某些潜在原因在项目层面的症状。该原因可能表现为行为气味,但最终可能存在潜在的代码气味,这是问题的根本原因。好消息:我们有三种不同的方法来识别问题。坏消息:很容易将注意力集中在某一层面的症状上,并试图直接解决该问题,而没有了解根本原因。

While we carefully delineated the various types of smells, it is important to note that very often we will observe symptoms of each kind of smell at the same time. Project smells, for example, are the project-level symptoms of some underlying cause. That cause may show up as a behavior smell but ultimately there is probably an underlying code smell that is the root cause of the problem. The good news: We have three different ways to identify a problem. The bad news: It is easy to focus on the symptom at one level and to try to solve that problem directly without understanding the root cause.

识别根本原因的一种非常有效的方法是“五个为什么” [TPS]。首先,我们问为什么会发生某件事。一旦我们确定了导致该事件的因素,我们接下来会问为什么会发生这些因素。我们重复这个过程,直到没有新的信息出现。实际上,问五次为什么通常就足够了——因此得名“五个为什么” 。1

A very effective technique for identifying the root cause is the "Five Why's" [TPS]. First, we ask why something is occurring. Once we have identified the factors that led to it, we next ask why each of those factors occurred. We repeat this process until no new information is forthcoming. In practice, asking why five times is usually enough—hence the name "Five Why's."1

在本章的其余部分,我们将研究我们在项目中最有可能遇到的与测试相关的异味。我们将从项目异味开始,然后逐步深入到导致这些异味的行为异味和代码异味。

In the rest of this chapter, we will look at the test-related smells that we are most likely to encounter on our projects. We will begin with the project smells, and then work our way down to the behavior smells and code smells that cause them.

气味目录

A Catalog of Smells

现在我们对测试异味及其在使用自动化测试的项目中的作用有了更好的了解,让我们来看看一些异味。根据前面概述的“可嗅性”标准,本节重点介绍这些异味。关于它们的原因和个别异味的描述将在本书第二部分讨论。

Now that we have a better understanding of test smells and their role in projects that use automated testing, let's look at some smells. Based on the "sniffability" criteria outlined earlier, this section focuses on introducing the smells. Discussions of their causes and the individual smell descriptions appear in Part II of this book.

项目有异味

The Project Smells

项目异味是项目出现问题的征兆。其根本原因可能是一个或多个代码或行为异味。然而,由于项目经理很少运行或编写测试,因此项目异味可能是他们得到的第一个暗示,表明测试自动化领域可能存在一些不完美的地方。

Project smells are symptoms that something has gone wrong on the project. Their root cause is likely to be one or more of the code or behavior smells. Because project managers rarely run or write tests, however, project smells are likely to be the first hint they get that something may be less than perfect in test automation land.

项目经理最关注的是功能、质量、资源和成本。因此,项目级异味往往集中在这些问题上。项目经理最有可能遇到的异味指标是软件质量,以正式测试或用户/客户发现的缺陷来衡量。如果生产缺陷(第268页) 的数量高于预期,项目经理必须问:“为什么所有这些缺陷都通过了我们的自动化测试安全网?”

Project managers focus most on functionality, quality, resources, and cost. For this reason, the project-level smells tend to cluster around these issues. The most obvious metric a project manager is likely to encounter as a smell is the quality of the software as measured in defects found in formal testing or by users/customers. If the number of Production Bugs (page 268) is higher than expected, the project manager must ask, "Why are all of these bugs getting through our safety net of automated tests?"

项目经理可能会监控每日集成构建失败的次数,以便尽早了解软件质量和团队开发流程的遵守情况。如果构建失败过于频繁,尤其是修复构建需要花费几分钟以上时间,经理可能会感到担忧。对失败的根本原因分析可能表明,许多测试失败不是软件错误导致的,而是源于有缺陷的测试(第260页)。这是一个测试大喊“狼来了!”并在纠正过程中消耗大量资源的例子,但实际上并没有提高生产代码的质量。

The project manager may be monitoring the number of times the daily integration build fails as a way of getting an early indication of software quality and adherence to the team's development process. The manager may become worried if the build fails too frequently, and especially if it takes more than a few minutes to fix the build. Root cause analysis of the failures may indicate that many of the test failures are not the result of buggy software but rather derive from Buggy Tests (page 260). This is an example in which the tests cry "Wolf!" and consume a lot of resources as part of their correction, but do not actually increase the quality of the production code.

测试缺陷只是导致高测试维护成本第 265页)这一更普遍问题的一个因素,如果不迅速解决,可能会严重影响团队的生产力。如果测试需要修改得太频繁(例如,每次修改 SUT 时),或者由于测试模糊导致修改测试的成本太高,项目经理可能会决定,将用于编写自动化测试的精力和费用花在编写更多生产代码或进行手动测试上会更好。此时,经理可能会告诉开发人员停止编写测试。2

Buggy Tests are just one contributor to the more general problem of High Test Maintenance Cost (page 265), which can severely affect the productivity of the team if not addressed quickly. If the tests need to be modified too often (e.g., every time the SUT is modified) or if the cost of modifying tests is too high due to Obscure Tests, the project manager may decide that the effort and expense being directed toward writing the automated tests would be better spent on writing more production code or doing manual testing. At this point, the manager is likely to tell the developers to stop writing tests.2

或者,项目经理可能认为生产缺陷是由开发人员没有编写测试(第 263页) 引起的。这种说法很可能在流程回顾期间或作为根本原因分析会议的一部分出现。开发人员没有编写测试可能是由于开发计划过于激进、主管告诉开发人员不要“浪费时间编写测试”,或者开发人员没有编写测试的技能。其他潜在原因可能包括不利于测试的强加设计或导致脆弱测试(第 239页) 的测试环境。最后,这个问题可能是由丢失的测试(请参阅生产缺陷) 引起的 - 存在但未包含在开发人员在签入或自动构建工具期间使用的AllTests 套件(请参阅第592页的命名测试套件) 中的测试。

Alternatively, the project manager may decide that the Production Bugs are caused by Developers Not Writing Tests (page 263). This pronouncement is likely to come during a process retrospective or as part of a root cause analysis session. Developers Not Writing Tests may be caused by an overly aggressive development schedule, supervisors who tell developers not to "waste time writing tests," or developers who do not have the skills to write tests. Other potential causes might include an imposed design that is not conducive to testing or a test environment that leads to Fragile Tests (page 239). Finally, this problem could result from Lost Tests (see Production Bugs)—tests that exist but are not included in the AllTests Suite (see Named Test Suite on page 592) used by developers during check-in or by the automated build tool.

行为气味

The Behavior Smells

行为异味是在编译或运行测试时遇到的。我们不需要特别细心就能注意到它们,因为这些异味会以编译错误或测试失败的形式出现。

Behavior smells are encountered when we compile or run tests. We don't have to be particularly observant to notice them, as these smells will take the form of compile errors or test failures.

最常见的行为异味是脆弱测试。当曾经通过的测试由于某种原因开始失败时,就会出现这种情况。脆弱测试问题在许多圈子里给测试自动化带来了坏名声,尤其是当商业“记录和回放”测试工具未能兑现其轻松进行测试自动化的承诺时。一旦记录下来,这些测试就很容易被破坏。通常唯一的补救办法是重新录制它们,因为测试记录很难理解或手动修改。

The most common behavior smell is Fragile Tests. It arises when tests that once passed begin failing for some reason. The Fragile Test problem has given test automation a bad name in many circles, especially when commercial "record and playback" test tools fail to deliver on their promise of easy test automation. Once recorded, these tests are very susceptible to breakage. Often the only remedy is to rerecord them because the test recordings are difficult to understand or modify by hand.

脆弱测试的根本原因可以分为四大类:

The root causes of Fragile Tests can be classified into four broad categories:

  • 界面敏感性(参见脆弱性测试)是指测试编程 API 或用于自动化测试的用户界面发生更改而导致测试中断。商业记录和回放测试(参见记录测试)工具通常通过用户界面与系统交互。即使界面发生微小更改也可能导致测试失败,即使在人类用户认为测试仍应通过的情况下也是如此。
  • Interface Sensitivity (see Fragile Test) occurs when tests are broken by changes to the test programming API or the user interface used to automate the tests. Commercial Record and Playback Test (see Recorded Test) tools typically interact with the system via the user interface. Even minor changes to the interface can cause tests to fail, even in circumstances in which a human user would say that the test should still pass.
  • 行为敏感性(参见脆弱性测试)是指测试因 SUT 行为的改变而中断。这看起来似乎是“理所当然的”(当然,如果我们更改 SUT,测试应该中断!)但问题是,任何一次更改都只应该中断少数测试。如果许多或大多数测试中断,我们就有麻烦了。
  • Behavior Sensitivity (see Fragile Test) occurs when tests are broken by changes to the behavior of the SUT. This may seem like a "no-brainer" (of course, the tests should break if we change the SUT!) but the issue is that only a few tests should be broken by any one change. If many or most of the tests break, we have a problem.
  • 数据敏感性(参见脆弱性测试)是指测试因 SUT 中已有数据的更改而中断。对于使用数据库的应用程序来说,这个问题尤其严重。数据敏感性是上下文敏感性(参见脆弱性测试)的一个特例,其中讨论的上下文是数据库。
  • Data Sensitivity (see Fragile Test) occurs when tests are broken by changes to the data already in the SUT. This issue is particularly a problem for applications that use databases. Data Sensitivity is a special case of Context Sensitivity (see Fragile Test) where the context in question is the database.
  • 当测试因 SUT 周围环境的差异而中断时,就会发生上下文敏感性。最常见的例子是测试依赖于时间或日期,但当测试依赖于服务器、打印机或显示器等设备的状态时,也会出现此问题。
  • Context Sensitivity occurs when tests are broken by differences in the environment surrounding the SUT. The most common example is when tests depend on the time or date, but this problem can also arise when tests rely on the state of devices such as servers, printers, or monitors.

数据敏感性上下文敏感性是脆弱测试(称为脆弱夹具)的一种特殊类型的例子,其中对常用测试夹具的更改会导致多个现有测试失败。这种情况增加了扩展标准夹具第 305页)以支持新测试的成本,进而阻碍了良好的测试覆盖率。虽然脆弱夹具的根本原因是测试设计不佳,但问题实际上是在夹具更改时出现的,而不是在 SUT 更改时出现的。

Data Sensitivity and Context Sensitivity are examples of a special kind of Fragile Test, known as a Fragile Fixture, in which changes to a commonly used test fixture cause multiple existing tests to fail. This scenario increases the cost of extending the Standard Fixture (page 305) to support new tests and, in turn, discourages good test coverage. Although Fragile Fixture's root cause is poor test design, the problem actually appears when the fixture is changed rather than when the SUT is changed.

大多数敏捷项目都使用某种形式的日常或持续集成,其中包括两个步骤:编译最新版本的代码并针对新编译的构建运行所有自动化测试。断言轮盘第 224页)可能难以确定集成构建期间测试失败的方式和原因,因为失败日志中没有包含足够的信息来清楚地识别哪个断言失败。构建失败的故障排除可能会进展缓慢,因为必须在开发环境中重现失败,然后我们才能推测失败的原因。

Most agile projects use some form of daily or continuous integration that includes two steps: compiling the latest version of the code and running all of the automated tests against the newly compiled build. Assertion Roulette (page 224) can make it difficult to determine how and why tests failed during the integration build because the failure log does not include sufficient information to clearly identify which assertion failed. Troubleshooting of the build failures may proceed slowly, because the failure must be reproduced in the development environment before we can speculate on the cause of the failure.

常见的麻烦是测试莫名失败。也就是说,测试和生产代码都没有修改,但测试却突然失败了。当我们尝试在开发环境中重现这些结果时,测试可能会失败,也可能不会失败。这些不稳定的测试第 228页)非常烦人,而且修复起来很费时间,因为它们可能有很多原因。下面列出了其中一些:

A common cause of grief is tests that fail for no apparent reason. That is, neither the tests nor the production code has been modified, yet the tests suddenly begin failing. When we try to reproduce these results in the development environment, the tests may or may not fail. These Erratic Tests (page 228) are both very annoying and time-consuming to fix, because they have numerous possible causes. A few are listed here:

  • 当多个测试使用共享装置(第 317页) 时,就会出现交互测试。它们使得单独运行测试或将多个测试套件作为更大的套件的一部分运行变得困难(请参阅第 387页的测试套件对象)。它们还可能导致级联故障 (单个测试失败会导致共享装置处于导致许多其他测试失败的状态)。
  • Interacting Tests arise when several tests use a Shared Fixture (page 317). They make it hard to run tests individually or to run several test suites as part of a larger Suite of Suites (see Test Suite Object on page 387). They can also cause cascading failures (where a single test failure leaves the Shared Fixture in a state that causes many other tests to fail).
  • 当多个测试运行者(第 377页)同时针对共享装置运行测试时,就会发生测试运行战争。它们总是在最糟糕的时候发生,例如当您试图修复发布前的最后几个错误时。
  • Test Run Wars occur when several Test Runners (page 377) run tests against a Shared Fixture at the same time. They invariably happen at the worst possible time, such as when you are trying to fix the last few bugs before a release.
  • 不可重复测试在第一次和后续测试运行之间提供不同的结果在测试运行之间执行手动干预(第250页)。
  • Unrepeatable Tests provide a different result between the first and subsequent test runs. They may force the test automater to perform a Manual Intervention (page 250) between test runs.

另一个降低生产率的异味是频繁调试第 248页)。自动化单元测试应该可以消除除极少数情况外使用调试器的需要,因为失败的测试集应该可以清楚地说明失败的原因。频繁调试表明单元测试覆盖范围不足或试图一次测试太多功能。

Another productivity-sapping smell is Frequent Debugging (page 248). Automated unit tests should obviate the need to use a debugger in all but rare cases, because the set of tests that are failing should make it obvious why the failure is occurring. Frequent Debugging is a sign that the unit tests are lacking in coverage or are trying to test too much functionality at once.

拥有全自动测试(第26页)的真正价值在于能够频繁运行它们。进行测试驱动开发的敏捷开发人员通常每隔几分钟运行一次测试(至少是一部分)。应该鼓励这种行为,因为它缩短了反馈循环,从而降低了引入代码的任何缺陷的成本。当测试每次运行时都需要人工干预时,开发人员倾向于降低测试运行频率。这种做法增加了查找自上次测试以来引入的所有缺陷的成本,因为自上次测试以来软件将进行更多更改。

The real value of having Fully Automated Tests (page 26) is being able to run them frequently. Agile developers who are doing test-driven development often run (at least a subset of) the tests every few minutes. This behavior should be encouraged because it shortens the feedback loop, thereby reducing the cost of any defects introduced into the code. When tests require Manual Intervention each time they are run, developers tend to run the tests less frequently. This practice increases the cost of finding all defects introduced since the tests were last run, because more changes will have been made to the software since it was last tested.

另一种对生产力有同样净影响的异味是慢速测试(第253页)。当测试运行时间超过大约 30 秒时,开发人员会在每次更改代码后停止运行测试,而是等待“逻辑时间”来运行它们 — 例如,在咖啡休息、午餐或会议之前。这种延迟反馈会导致“流程”的损失,并增加缺陷引入和测试识别之间的时间。慢速测试最常用的解决方案也是最成问题的;共享装置可能导致许多行为异味,应该是最后的解决办法。

Another smell that has the same net impact on productivity is Slow Tests (page 253). When tests take more than approximately 30 seconds to run, developers stop running them after every individual code change, instead waiting for a "logical time" to run them—for example, before a coffee break, lunch, or a meeting. This delayed feedback results in a loss of "flow" and increases the time between when a defect is introduced and when it is identified by a test. The most frequently used solution to Slow Tests is also the most problematic; a Shared Fixture can result in many behavior smells and should be the solution of last resort.

代码异味

The Code Smells

代码异味是 Martin Fowler 在《重构》 [Ref]中首次描述的“经典”坏味道。事实上,Fowler 识别的大多数味道都是代码异味。测试自动化人员在维护测试代码时必须识别这些味道。虽然代码异味通常会影响测试的维护成本,但它们也可能是需要关注的行为异味的早期预警信号。

Code smells are the "classic" bad smells that were first described by Martin Fowler in Refactoring [Ref]. Indeed, most of the smells identified by Fowler are code smells. These smells must be recognized by test automaters as they maintain test code. Although code smells typically affect maintenance cost of tests, they may also be early warning signs of behavior smells to follow.

在阅读测试时,一个相当明显(尽管经常被忽视)的异味是模糊测试。它可以有多种形式,但所有版本都有相同的影响:很难说出测试试图做什么,因为测试没有传达意图(第41页)。这种模糊性增加了测试维护的成本,并且当测试维护者对测试进行错误更改时,可能会导致错误测试。

When reading tests, a fairly obvious—albeit often overlooked—smell is Obscure Test. It can take many forms, but all versions have the same impact: It becomes difficult to tell what the test is trying to do, because the test does not Communicate Intent (page 41). This ambiguity increases the cost of test maintenance and can lead to Buggy Tests when a test maintainer makes the wrong change to the test.

相关的气味是条件测试逻辑第 200页)。测试应该是简单的线性语句序列。当测试有多个执行路径时,我们无法确定测试在特定情况下将如何执行。

A related smell is Conditional Test Logic (page 200). Tests should be simple, linear sequences of statements. When tests have multiple execution paths, we cannot be sure exactly how the test will execute in a specific case.

硬编码测试数据(参见模糊测试)可能因多种原因而变得阴险。首先,它使测试更难理解:我们需要查看每个值并猜测它是否与其他值相关,以了解 SUT 应该如何表现。其次,当我们测试包含数据库的 SUT 时,它会造成挑战。硬编码测试数据可能导致不稳定的测试(如果测试恰好使用相同的数据库密钥)或脆弱的装置(如果值引用数据库中已更改的记录)。

Hard-Coded Test Data (see Obscure Test) can be insidious for several reasons. First, it makes tests more problematic to understand: We need to look at each value and guess whether it is related to any of the other values to understand how the SUT is supposed to behave. Second, it creates challenges when we are testing a SUT that includes a database. Hard-Coded Test Data can lead to Erratic Tests (if tests happen to use the same database key) or Fragile Fixtures (if the values refer to records in the database that have been changed).

难以测试的代码第 209)可能是导致许多其他代码和行为异味的一个因素。这个问题对于编写测试但无法找到设置夹具、运行 SUT 或验证预期结果的方法的人来说最为明显。然后,测试自动化人员可能会被迫测试比他或她想要的更多的软件(由许多类组成的更大的 SUT)。在阅读测试时,难以测试的代码往往会显示为模糊测试,因为测试自动化人员必须跳过一些环节才能与 SUT 交互。

Hard-to-Test Code (page 209) may be a contributing factor to a number of other code and behavior smells. This problem is most obvious to the person who is writing a test and cannot find a way to set up the fixture, exercise the SUT, or verify the expected outcome. The test automater may then be forced to test more software (a larger SUT consisting of many classes) than he or she would like. When reading a test, Hard-to-Test Code tends to show up as an Obscure Test because of the hoops the test automater had to jump through to interact with the SUT.

测试代码重复(第 213) 是一种不好的做法,因为它会增加维护测试的成本。我们有更多的测试代码需要维护,而这些代码更难维护,因为它经常与模糊测试重合。当自动化测试人员克隆测试而没有对如何智能地重用测试逻辑进行足够思考时,通常就会出现重复。3随着测试需求的出现,测试自动化程序将常用的语句序列分解为测试实用程序方法(第 599页) 非常重要,这些语句序列可以被各种测试方法(第 348)重用。4这种做法通过多种方式降低了测试的维护成本。

Test Code Duplication (page 213) is a poor practice because it increases the cost of maintaining tests. We have more test code to maintain and that code is more challenging to maintain because it often coincides with an Obscure Test. Duplication often arises when the automated tester clones tests and does not put enough thought into how to reuse test logic intelligently.3 As testing needs emerge, it is important that the test automater factor out commonly used sequences of statements into Test Utility Methods (page 599) that can be reused by various Test Methods (page 348).4 This practice reduces the maintenance cost of tests in several ways.

在生产环境中测试逻辑(第 217) 是不可取的,因为没有办法确保它不会意外运行。5它还会使生产代码变得更大、更复杂。最后,此错误可能会导致其他软件组件或库被包含在可执行文件中。

Test Logic in Production (page 217) is undesirable because there is no way to ensure that it will not run accidentally.5 It also makes the production code larger and more complicated. Finally, this error may cause other software components or libraries to be included in the executable.

下一步是什么?

What's Next?

在本章中,我们看到了自动化测试时可能出错的大量事情。第 3 章测试自动化的目标”描述了我们在自动化测试时需要牢记的目标,以便我们能够获得有效的测试自动化体验。这种理解将帮助我们了解有助于我们避免本章中描述的许多问题的原则。

In this chapter, we saw a plethora of things that can go wrong when automating tests. Chapter 3, Goals of Test Automation, describes the goals we need to keep in mind while automating tests so that we can have an effective test automation experience. That understanding will prepare us to look at the principles that will help us steer clear of many of the problems described in this chapter.

第 3 章

测试自动化的目标

Chapter 3

Goals of Test Automation

 

关于本章

About This Chapter

第 2 章测试异味”介绍了各种“测试异味”,它们可能是自动化测试问题的症状。本章描述了我们应努力实现的目标,以确保自动化单元测试和客户测试成功。它首先概述了我们为什么要进行自动化测试,然后描述了测试自动化的总体目标,包括降低成本、提高质量和提高对代码的理解。每个领域都有更详细的目标,本文也将简要讨论这些目标。本章不描述如何实现这些目标;后续章节将介绍如何实现这些目标,这些目标将用作许多原则和模式的理论基础。

Chapter 2, Test Smells, introduced the various "test smells" that can act as symptoms of problems with automated testing. This chapter describes the goals we should be striving to reach to ensure successful automated unit tests and customer tests. It begins with a general discussion of why we automate tests, then turns to a description of the overall goals of test automation, including reducing costs, improving quality, and improving the understanding of code. Each of these areas has more detailed named goals that are discussed briefly here as well. This chapter doesn't describe how to achieve these goals; that explanation will come in subsequent chapters where these goals are used as the rationale for many of the principles and patterns.

为什么要测试?

Why Test?

关于自动化单元测试和验收测试作为敏捷软件开发的一部分的必要性,已经有很多文章进行了论述。编写好的测试代码很难,维护晦涩难懂的测试代码就更难了。由于测试代码是可选的(即,它不是客户要支付的费用),因此当测试变得难以维护或维护成本高昂时,人们很容易放弃测试。一旦我们放弃了“保持绿色条形码以保持代码清洁”的原则,自动化测试的大部分价值就会丧失。

Much has been written about the need for automated unit and acceptance tests as part of agile software development. Writing good test code is hard, and maintaining obtuse test code is even harder. Because test code is optional (i.e., it is not what the customer is paying for), there is a strong temptation to abandon testing when the tests become difficult or expensive to maintain. Once we have given up on the principle of "keep the bar green to keep the code clean," much of the value of the automated tests is lost.

在一系列项目中,我与之合作的团队在自动化测试方面面临许多挑战。编写和维护测试套件的成本是一个特别的挑战,尤其是对于包含数千个测试的项目。幸运的是,正如陈词滥调所说,“需要是发明之母”。我的团队和其他团队已经开发出许多解决方案来应对这些挑战。从那时起,我花了很多时间思考这些解决方案,问自己为什么它们是好的解决方案。在此过程中,我将成功解决方案的组成部分分为目标(要实现的事情)和原则(实现它们的方法)。遵守这些目标和原则将使自动化测试更易于编写、阅读和维护。

Over a series of projects, the teams I have worked with have faced a number of challenges to automated testing. The cost of writing and maintaining test suites has been a particular challenge, especially on projects with thousands of tests. Fortunately, as the cliché says, "Necessity is the mother of invention." My teams, and others, have developed a number of solutions to address these challenges. I have since spent a lot of time reflecting on these solutions to ask why they are good solutions. Along the way, I have divided the components of successful solutions into goals (things to achieve) and principles (ways to achieve them). Adherence to these goals and principles will result in automated tests that are easier to write, read, and maintain.

测试自动化的经济学

Economics of Test Automation

当然,构建和维护自动化测试套件总是需要成本的。热心的测试自动化倡导者会认为,花更多的钱来获得以后更改软件的能力是值得的。不幸的是,在艰难的经济环境下,这种“现在付钱给我,以后就不用付钱给我”的说法行不通。1

Of course, there is always a cost incurred in building and maintaining an automated test suite. Ardent test automation advocates will argue that it is worth spending more to have the ability to change the software later. Unfortunately, this "pay me now so you don't have to pay me later" argument doesn't go very far in a tough economic climate.1

我们的目标应该是确保测试自动化不会增加软件开发成本,从而使实施测试自动化的决定变得“轻而易举”。因此,构建和维护自动化测试的额外成本必须通过减少手动单元测试和调试/故障排除以及在项目的正式测试阶段或应用程序的早期生产使用之前未发现的缺陷的补救成本来抵消。图 3.1显示了自动化成本如何通过自动化节省的成本来抵消。

Our goal should be to make the decision to do test automation a "no-brainer" by ensuring that it does not increase the cost of software development. Thus the additional cost of building and maintaining automated tests must be offset by savings through reduced manual unit testing and debugging/troubleshooting as well as the remediation cost of the defects that would have gone undetected until the formal test phase of the project or early production usage of the application. Figure 3.1 shows how the cost of automation is offset by the savings received from automation.

图 3.1. 具有良好投资回报的自动化单元测试项目。通过良好的测试实践降低总成本时的成本效益权衡。

Figure 3.1. An automated unit test project with a good return on investment. The cost-benefit trade-off when the total cost is reduced by good test practices.

图像

最初,学习新技术和实践的成本需要额外的努力。然而,一旦我们克服了这个“障碍”,我们就应该安定下来,达到一个稳定的状态,即增加的成本(线上方的部分)完全被节省的成本(线下方的部分)抵消。如果测试难以编写、难以理解,并且需要频繁且昂贵的维护,那么软件开发的总成本(垂直箭头的高度)就会上升,如图3.2所示。

Initially, the cost of learning the new technology and practices takes additional effort. Once we get over this "hump," however, we should settle down to a steady state where the added cost (the part above the line) is fully offset by the savings (the part below the line). If tests are difficult to write, are difficult to understand, and require frequent, expensive maintenance, the total cost of software development (the heights of the vertical arrows) goes up as illustrated in Figure 3.2.

图 3.2. 投资回报率低的自动化单元测试项目。由于测试实践不佳而导致总成本增加时的成本收益权衡。

Figure 3.2. An automated unit test project with a poor return on investment. The cost-benefit trade-off when the total cost is increased by poor test practices.

图像

请注意,图 3.2中线上方增加的工作量比图 3.1中增加的工作量要多,并且随着时间的推移还在不断增加。此外,线下方节省的工作量减少了。这反映了总体工作量的增加,超过了没有测试自动化时的原始工作量。

Note how the added work above the line in Figure 3.2 is more than that seen in Figure 3.1 and continues to increase over time. Also, the saved effort below the line is reduced. This reflects the increase in overall effort, which exceeds the original effort without test automation.

测试自动化的目标

Goals of Test Automation

我们所有人都对测试自动化有一些想法,认为自动化测试是一件“好事”。以下是一些可能适用的高级目标:

We all come to test automation with some notion of why having automated tests would be a "good thing." Here are some high-level objectives that might apply:

  • 测试应该可以帮助我们提高质量。
  • Tests should help us improve quality.
  • 测试应该有助于我们理解 SUT。
  • Tests should help us understand the SUT.
  • 测试应该减少(而不是引入)风险。
  • Tests should reduce (and not introduce) risk.
  • 测试应该易于运行。
  • Tests should be easy to run.
  • 测试应该易于编写和维护。
  • Tests should be easy to write and maintain.
  • 随着系统的发展,测试应该只需要最少的维护。
  • Tests should require minimal maintenance as the system evolves around them.

前三个目标展示了测试提供的价值,而后三个目标则侧重于测试本身的特性。大多数这些目标都可以分解为更具体(和可衡量)的目标。我给了这些简短易记的名字,这样我就可以将它们称为特定原则或模式的动机。

The first three objectives demonstrate the value provided by the tests, whereas the last three objectives focus on the characteristics of the tests themselves. Most of these objectives can be decomposed into more concrete (and measurable) goals. I have given these short catchy names so that I can refer to them as motivators of specific principles or patterns.

测试应该帮助我们提高质量

Tests Should Help Us Improve Quality

进行测试的传统原因是为了质量保证 (QA)。那么,我们究竟是什么意思呢?什么是质量?传统的定义根据以下问题将质量分为两大类:(1) 软件是否正确构建?(2) 我们是否构建了正确的软件?

The traditional reason given for doing testing is for quality assurance (QA). What, precisely, do we mean by this? What is quality? Traditional definitions distinguish two main categories of quality based on the following questions: (1) Is the software built correctly? and (2) Have we built the right software?

目标:测试即规范

也称为

Also known as

可执行规范

Executable Specification

如果我们正在进行测试驱动开发或测试优先开发,测试将为我们提供一种在开始构建 SUT 之前捕获其应该执行的操作的方法。它们使我们能够以可以执行的形式(本质上是“可执行规范”)指定捕获的各种场景中的行为。为了确保我们“构建正确的软件”,我们必须确保我们的测试反映了 SUT 的实际使用方式。可以通过开发用户界面模型来促进这项工作,这些模型可以捕获有关应用程序外观和行为的足够细节,以便我们编写测试。

If we are doing test-driven development or test-first development, the tests give us a way to capture what the SUT should be doing before we start building it. They enable us to specify the behavior in various scenarios captured in a form that we can then execute (essentially an "executable specification"). To ensure that we are "building the right software," we must ensure that our tests reflect how the SUT will actually be used. This effort can be facilitated by developing user interface mockups that capture just enough detail about how the application appears and behaves so that we can write our tests.

仔细思考各种场景并将其转化为测试,这一行为本身有助于我们识别需求模糊或自相矛盾的地方。这种分析可以提高规范的质量,从而提高所规定的软件的质量。

The very act of thinking through various scenarios in enough detail to turn them into tests helps us identify those areas where the requirements are ambiguous or self-contradictory. Such analysis improves the quality of the specification, which improves the quality of the software so specified.

目标:驱虫

是的,测试确实能发现错误,但自动化测试的目的并非如此。自动化测试旨在防止错误被引入。自动化测试可以视为“防错误剂”,在我们确保软件中不存在任何错误后,它可以防止令人讨厌的小错误再次爬进我们的软件中。只要我们进行回归测试,就不会出现错误,因为测试会在我们签入代码之前指出错误。(我们在每次签入之前都会运行所有测试,不是吗?)

Yes, tests find bugs—but that really isn't what automated testing is about. Automated testing tries to prevent bugs from being introduced. Think of automated tests as "bug repellent" that keeps nasty little bugs from crawling back into our software after we have made sure it doesn't contain any bugs. Wherever we have regression tests, we won't have bugs because the tests will point the bugs out before we even check in our code. (We are running all the tests before every check-in, aren't we?)

目标:缺陷定位

错误总是会发生!当然,有些错误的预防成本比修复成本高得多。假设一个错误不知何故溜了出来,出现在集成构建 [SCM] 中。如果我们的单元测试相当小(即,我们在每个测试中只测试一个行为),我们应该能够根据哪个测试失败来快速查明错误。这种特异性是单元测试相对于客户测试的主要优势之一。客户测试告诉我们客户期望的某些行为不起作用;单元测试告诉我们原因。我们将这种现象称为缺陷定位。如果客户测试失败但没有单元测试失败,则表示缺少单元测试(请参阅第268页的生产错误)。

Mistakes happen! Of course, some mistakes are much more expensive to prevent than to fix. Suppose a bug does slip through somehow and shows up in the Integration Build [SCM]. If our unit tests are fairly small (i.e., we test only a single behavior in each one), we should be able to pinpoint the bug quickly based on which test fails. This specificity is one of the major advantages that unit tests enjoy over customer tests. The customer tests tell us that some behavior expected by the customer isn't working; the unit tests tell us why. We call this phenomenon Defect Localization. If a customer test fails but no unit tests fail, it indicates a Missing Unit Test (see Production Bugs on page 268).

所有这些好处都很棒,但如果我们不为每个软件单元需要覆盖的所有可能场景编写测试,我们就无法实现这些好处。如果测试本身包含错误,我们也无法实现这些好处。显然,我们必须让测试尽可能简单,以便人们能够轻松看到它们是正确的。虽然为我们的单元测试编写单元测试不是一个实用的解决方案,但我们可以(也应该)为任何测试实用程序方法第 599页)编写单元测试,我们将测试方法所需的复杂算法委托给这些方法。

All of these benefits are wonderful—but we cannot achieve them if we don't write tests for all possible scenarios that each unit of software needs to cover. Nor will we realize these benefits if the tests themselves contain bugs. Clearly, it is crucial that we keep the tests as simple as possible so that they can be easily seen to be correct. While writing unit tests for our unit tests is not a practical solution, we can—and should—write unit tests for any Test Utility Method (page 599) to which we delegate complex algorithms needed by the test methods.

测试应该帮助我们理解 SUT

Tests Should Help Us Understand the SUT

测试能为我们做的不仅仅是消除错误。它们还可以向测试读者展示代码应该如何工作。黑盒组件测试实际上描述了软件组件的要求。

Repelling bugs isn't the only thing the tests can do for us. They can also show the test reader how the code is supposed to work. Black box component tests are—in effect—describing the requirements of that of software component.

目标:测试即文档

如果没有自动化测试,我们就需要仔细研究 SUT 代码,试图回答这个问题:“如果……结果应该是什么?”有了自动化测试,我们只需将相应的测试用作文档;它们会告诉我们结果应该是什么(回想一下,自检测试在一个或多个断言中陈述了预期结果)。如果我们想知道系统如何执行某项操作,我们可以打开调试器,运行测试,然后单步执行代码以查看其工作原理。从这个意义上讲,自动化测试充当了 SUT 的一种文档形式。

Without automated tests, we would need to pore over the SUT code trying to answer the question, "What should be the result if . . . ?" With automated tests, we simply use the corresponding Tests as Documentation; they tell us what the result should be (recall that a Self-Checking Test states the expected outcome in one or more assertions). If we want to know how the system does something, we can turn on the debugger, run the test, and single-step through the code to see how it works. In this sense, the automated tests act as a form of documentation for the SUT.

测试应该减少(而不是引入)风险

Tests Should Reduce (and Not Introduce) Risk

如前所述,测试应该通过帮助我们更好地记录需求并防止在增量开发过程中出现错误来提高软件的质量。这当然是降低风险的一种形式。其他形式的风险降低涉及在“不可能”的情况下验证软件的行为,而当对整个应用程序进行传统的黑盒客户测试时,这些情况是无法引发的。审查项目的所有风险并集思广益,看看哪些风险至少可以通过使用全自动测试来部分缓解,这是非常有用的练习。

As mentioned earlier, tests should improve the quality of our software by helping us better document the requirements and prevent bugs from creeping in during incremental development. This is certainly one form of risk reduction. Other forms of risk reduction involve verifying the software's behavior in the "impossible" circumstances that cannot be induced when doing traditional customer testing of the entire application as a black box. It is a very useful exercise to review all of the project's risks and brainstorm about which kinds of risks could be at least partially mitigated through the use of Fully Automated Tests.

目标:测试作为安全网

也称为

Also known as

安全网

Safety Net

在处理遗留代码时,我总是感到紧张。顾名思义,遗留代码没有一套自动回归测试。更改这种代码是有风险的,因为我们永远不知道我们可能会破坏什么,而且我们无法知道我们是否破坏某些东西!因此,我们必须非常缓慢和谨慎地工作,在进行任何更改之前进行大量的手动分析。

When working on legacy code, I always feel nervous. By definition, legacy code doesn't have a suite of automated regression tests. Changing this kind of code is risky because we never know what we might break, and we have no way of knowing whether we have broken something! As a consequence, we must work very slowly and carefully, doing a lot of manual analysis before making any changes.

相比之下,使用具有回归测试套件的代码时,我们可以更快地完成工作。我们可以采用更具实验性的方法来更改软件:“我想知道如果我更改了它会发生什么?哪些测试失败了?有趣!所以这就是这个参数的用途。”这样,自动化测试就充当了安全网,让我们可以冒险。2

When working with code that has a regression test suite, by contrast, we can work much more quickly. We can adopt a more experimental style of changing the software: "I wonder what would happen if I changed this? Which tests fail? Interesting! So that's what this parameter is for." In this way, the automated tests act as a safety net that allows us to take chances.2

安全网的有效性取决于我们的测试对系统行为的验证程度。缺少测试就像安全网上的漏洞。不完整的断言就像断线。安全网中的每个漏洞都可能让各种大小的错误通过。

The effectiveness of the safety net is determined by how completely our tests verify the behavior of the system. Missing tests are like holes in the safety net. Incomplete assertions are like broken strands. Each gap in the safety net can let bugs of various sizes through.

现代软件开发环境的版本控制功能增强了安全网的有效性。如果我们的测试表明当前的更改对代码的影响太大,那么 CVS、Subversion 或 SourceSafe 等源代码存储库 [SCM] 可让我们将更改回滚到已知点。IDE 的内置“撤消”或“本地历史记录”功能让我们将时钟回退 5 秒、5 分钟甚至 5 小时。

The effectiveness of the safety net is amplified by the version-control capabilities of modern software development environments. A source code repository [SCM] such as CVS, Subversion, or SourceSafe lets us roll back our changes to a known point if our tests suggest that the current set of changes is affecting the code too extensively. The built-in "undo" or "local history" features of the IDE let us turn the clock back 5 seconds, 5 minutes, or even 5 hours.

目标:不造成伤害

也称为

Also known as

无测试风险

No Test Risk

当然,这个讨论还有另一面:自动化测试如何会带来风险?我们必须小心,不要因为进行自动化测试而给 SUT 带来新的问题。“将测试逻辑排除在生产代码之外”原则指导我们避免将测试特定的钩子放入 SUT 中。设计可测试的系统当然是可取的,但任何测试特定的代码都应该由测试插入,并且只能在测试环境中插入;在生产时,它不应该存在于 SUT 中。

Naturally, there is a flip side to this discussion: How might automated tests introduce risk? We must be careful not to introduce new kinds of problems into the SUT as a result of doing automated testing. The Keep Test Logic Out of Production Code principle directs us to avoid putting test-specific hooks into the SUT. It is certainly desirable to design the system for testability, but any test-specific code should be plugged in by the test and only in the test environment; it should not exist in the SUT when it is in production.

另一种风险是相信某些代码是可靠的,因为它已经过彻底测试,但事实上并非如此。刚开始使用测试替身第 522页)的开发人员常犯的一个错误是用测试替身替换太多的 SUT 。这引出了另一个重要原则:不要修改 SUT。也就是说,我们必须清楚我们正在测试哪个 SUT,并避免用测试特定逻辑替换我们正在测试的部分(图 3.3)。

Another form of risk is believing that some code is reliable because it has been thoroughly tested when, in fact, it has not. A common mistake made by developers new to the use of Test Doubles (page 522) is replacing too much of the SUT with a Test Double. This leads to another important principle: Don't Modify the SUT. That is, we must be clear about which SUT we are testing and avoid replacing the parts we are testing with test-specific logic (Figure 3.3).

图 3.3。 一系列测试,每个测试都有自己的 SUT。应用程序、组件或单元只是特定测试集的 SUT。“Unit1 SUT”充当“Unit2 Test”的 DOC(夹具的一部分),是“Comp1 SUT”的一部分。

Figure 3.3. A range of tests, each with its own SUT. An application, component, or unit is only the SUT with respect to a specific set of tests. The "Unit1 SUT" plays the role of DOC (part of the fixture) to the "Unit2 Test" and is part of the "Comp1 SUT."

图像

测试应该易于运行

Tests Should Be Easy to Run

大多数软件开发人员只想编写代码;测试只是我们工作中必不可少的恶事。自动化测试提供了一个很好的安全网,让我们可以更快地编写代码,3但只有当自动化测试确实易于运行时,我们才会频繁运行它们。

Most software developers just want to write code; testing is simply a necessary evil in our line of work. Automated tests provide a nice safety net so that we can write code more quickly,3 but we will run the automated tests frequently only if they are really easy to run.

什么使测试更容易运行?四个具体目标回答了这个问题:

What makes tests easy to run? Four specific goals answer this question:

  • 它们必须是完全自动化的测试,这样才能毫不费力地运行。
  • They must be Fully Automated Tests so they can be run without any effort.
  • 它们必须是自检测试,这样它们就可以无需人工检查就检测并报告任何错误。
  • They must be Self-Checking Tests so they can detect and report any errors without manual inspection.
  • 它们必须是可重复测试,以便可以多次运行并获得相同的结果。
  • They must be Repeatable Tests so they can be run multiple times with the same result.
  • 理想情况下,每个测试都应该是可以自行运行的独立测试。
  • Ideally, each test should be an Independent Test that can be run by itself.

满足这四个目标后,只需单击一下按钮(或键盘快捷键)即可获得测试提供的宝贵反馈。让我们更详细地了解一下这些目标。

With these four goals satisfied, one click of a button (or keyboard shortcut) is all it should take to get the valuable feedback the tests provide. Let's look at these goals in a bit more detail.

目标:完全自动化测试

无需任何人工干预第 250页)即可运行的测试是全自动测试。满足此标准是实现许多其他目标的先决条件。是的,可以编写不检查结果并且仅运行一次的全自动测试main()。运行代码并将打印语句定向到控制台的程序就是此类测试的一个很好的示例。我认为测试自动化的这两个方面对于使测试易于运行非常重要,因此我将它们设为单独的目标:自检测试可重复测试

A test that can be run without any Manual Intervention (page 250) is a Fully Automated Test. Satisfying this criterion is a prerequisite to meeting many of the other goals. Yes, it is possible to write Fully Automated Tests that don't check the results and that can be run only once. The main() program that runs the code and directs print statements to the console is a good example of such a test. I consider these two aspects of test automation to be so important in making tests easy to run that I have made them separate goals: Self-Checking Test and Repeatable Test.

目标:自我检查测试

自检测试已将测试所需的一切编码到其中,以验证预期结果是否正确。自检测试将好莱坞原则(“不要给我们打电话,我们会给您打电话”)应用于运行测试。也就是说,测试运行器第 377页)仅在测试通过时才“给我们打电话”;因此,干净的测试运行不需要任何手动操作。xUnit 系列的许多成员都提供了图形测试运行器(请参阅测试运行器),它使用绿色条表示一切“正常”;红色条表示测试失败并需要进一步调查。

A Self-Checking Test has encoded within it everything that the test needs to verify that the expected outcome is correct. Self-Checking Tests apply the Hollywood principle ("Don't call us; we'll call you") to running tests. That is, the Test Runner (page 377) "calls us" only when a test did not pass; as a consequence, a clean test run requires zero manual effort. Many members of the xUnit family provide a Graphical Test Runner (see Test Runner) that uses a green bar to signal that everything is "A-okay"; a red bar indicates that a test has failed and warrants further investigation.

目标:可重复测试

可重复测试可以连续运行多次,并且每次运行之间无需人工干预即可产生完全相同的结果。不可重复测试(请参阅第228页的不稳定测试)会显著增加运行测试的开销。这种结果非常不受欢迎,因为我们希望所有开发人员都能非常频繁地运行测试——每次“保存”后都运行一次。不可重复测试只能运行一次,运行测试的人员必须执行手动干预。同样糟糕的是非确定性测试(请参阅不稳定测试),它们在不同时间产生不同的结果;它们迫使我们花费大量时间追踪失败的测试。当我们毫无理由地经常看到红条时,它的威力会大大减弱。很快,我们开始忽略红条,以为只要等待足够长的时间,它就会消失。一旦发生这种情况,我们就失去了自动化测试的很多价值,因为表明我们引入了一个错误并立即修复它的反馈消失了。我们等待的时间越长,找到测试失败的根源所需的精力就越大。

A Repeatable Test can be run many times in a row and will produce exactly the same results without any human intervention between runs. Unrepeatable Tests (see Erratic Test on page 228) increase the overhead of running tests significantly. This outcome is very undesirable because we want all developers to be able to run the tests very frequently—as often as after every "save." Unrepeatable Tests can be run only once before whoever is running the tests must perform a Manual Intervention. Just as bad are Nondeterministic Tests (see Erratic Test) that produce different results at different times; they force us to spend lots of time chasing down failing tests. The power of the red bar diminishes significantly when we see it regularly without good reason. All too soon, we begin ignoring the red bar, assuming that it will go away if we wait long enough. Once this happens, we have lost a lot of the value of our automated tests, because the feedback indicating that we have introduced a bug and should fix it right away disappears. The longer we wait, the more effort it takes to find the source of the failing test.

仅在内存中运行且仅使用局部变量或字段的测试通常可重复,无需我们付出任何额外努力。不可重复的测试通常是因为我们使用了某种共享装置(此定义包括在SUT内实现的任何数据持久性)。在这种情况下,我们必须确保我们的测试也是“自我清理”的。当需要清理时,最一致和最万无一失的策略是使用通用的自动拆卸机制。虽然可以为每个测试编写拆卸代码,但如果没有在每个测试中正确实现,这种方法可能会导致不稳定测试。

Tests that run only in memory and that use only local variables or fields are usually repeatable without us expending any additional effort. Unrepeatable Tests usually come about because we are using a Shared Fixture (page 317) of some sort (this definition includes any persistence of data implemented within the SUT). In such a case, we must ensure that our tests are "self-cleaning" as well. When cleaning is necessary, the most consistent and foolproof strategy is to use a generic Automated Teardown (page 503) mechanism. Although it is possible to write teardown code for each test, this approach can result in Erratic Tests when it is not implemented correctly in every test.

测试应该易于编写和维护

Tests Should Be Easy to Write and Maintain

编码从根本上来说是一项困难的活动,因为我们必须在工作时记住大量的信息。编写测试时,我们应该专注于测试而不是测试的编码。这意味着测试必须简单——易于阅读编写。它们需要易于阅读和理解,因为测试自动化测试本身就是一项复杂的工作。只有通过将要检测的错误引入 SUT 才能对其进行适当的测试;这很难以自动化的方式完成,因此通常只在首次编写测试时进行一次(如果有的话)。出于这些原因,我们需要依靠我们的眼睛来发现测试中出现的任何问题,这意味着我们必须保持测试足够简单,以便快速阅读。

Coding is a fundamentally difficult activity because we must keep a lot of information in our heads as we work. When we are writing tests, we should stay focused on testing rather than coding of the tests. This means that tests must be simple—simple to read and simple to write. They need to be simple to read and understand because testing the automated tests themselves is a complicated endeavor. They can be tested properly only by introducing the very bugs that they are intended to detect into the SUT; this is hard to do in an automated way so it is usually done only once (if at all), when the test is first written. For these reasons, we need to rely on our eyes to catch any problems that creep into the tests, and that means we must keep the tests simple enough to read quickly.

当然,如果我们要改变系统部分的行为,我们应该预料到只有少数测试会受到修改的影响。我们希望尽量减少测试重叠,以便只有少数测试会受到任何一次更改的影响。与普遍的看法相反,如果大多数测试都做同样的事情,那么让更多测试通过相同的代码并不能提高代码的质量。

Of course, if we are changing the behavior of part of the system, we should expect a small number of tests to be affected by our modifications. We want to Minimize Test Overlap so that only a few tests are affected by any one change. Contrary to popular opinion, having more tests pass through the same code doesn't improve the quality of the code if most of the tests do exactly the same thing.

测试变得复杂有两个原因:

Tests become complicated for two reasons:

  • 我们尝试在一次测试中验证太多的功能。
  • We try to verify too much functionality in a single test.
  • 过大的“表达能力差距”将测试脚本语言(例如 Java)与我们试图在测试中表达的领域概念之间的前/后关系区分开来。
  • Too large an "expressiveness gap" separates the test scripting language (e.g., Java) and the before/after relationships between domain concepts that we are trying to express in the test.
目标:简单测试

为了避免“贪多嚼不烂”,我们的测试应该很小,一次只测试一件事。在测试驱动开发中,保持测试简单尤为重要,因为代码是一次通过一个测试编写的,我们希望每个测试只向 SUT 引入一个新的行为。我们应努力通过为每个独特的预测试状态和输入组合创建单独的测试方法(第348页) 来验证每个测试的一个条件。每个测试方法都应通过单个代码路径驱动 SUT。4

To avoid "biting off more than they can chew," our tests should be small and test one thing at a time. Keeping tests simple is particularly important during test-driven development because code is written to pass one test at a time and we want each test to introduce only one new bit of behavior into the SUT. We should strive to Verify One Condition per Test by creating a separate Test Method (page 348) for each unique combination of pre-test state and input. Each Test Method should drive the SUT through a single code path.4

要求测试方法简短的主要例外是客户测试,这些测试表达了应用程序的真实使用场景。这种扩展测试提供了一种有用的方式来记录软件的潜在用户将如何使用它;如果这些交互涉及长序列的步骤,测试方法应该反映这一现实。

The major exception to the mandate to keep Test Methods short occurs with customer tests that express real usage scenarios of the application. Such extended tests offer a useful way to document how a potential user of the software would go about using it; if these interactions involve long sequences of steps, the Test Methods should reflect this reality.

目标:表达性测试

可以通过构建一个测试实用方法库来解决“表达能力差距” ,这些方法构成了特定领域的测试语言。这种方法集合允许测试自动化人员表达他们希望测试的概念,而不必将他们的想法转化为更详细的代码。创建方法第 415页)和自定义断言(第474页)是构成这种高级语言的构建块的很好的例子。

The "expressiveness gap" can be addressed by building up a library of Test Utility Methods that constitute a domain-specific testing language. Such a collection of methods allows test automaters to express the concepts that they wish to test without having to translate their thoughts into much more detailed code. Creation Methods (page 415) and Custom Assertion (page 474) are good examples of the building blocks that make up such a Higher-Level Language.

解决这一困境的关键是避免测试中的重复。务实程序员 ( http://www.pragmaticprogrammer.com ) 的 DRY 原则(“不要重复自己”)应像应用于生产代码一样应用于测试代码。然而,这其中存在着一种反作用力。由于测试应该传达意图,因此最好将核心测试逻辑保留在每个测试方法中,以便可以在一个地方看到它。尽管如此,这个想法并不妨碍将大量支持代码移到测试实用程序方法中,如果受 SUT 更改的影响,则只需在一个地方对其进行修改。

The key to solving this dilemma is avoiding duplication within tests. The DRY principle—"Don't repeat yourself"—of the Pragmatic Programmers (http://www.pragmaticprogrammer.com) should be applied to test code in the same way it is applied to production code. There is, however, a counterforce at play. Because the tests should Communicate Intent, it is best to keep the core test logic in each Test Method so it can be seen in one place. Nevertheless, this idea doesn't preclude moving a lot of supporting code into Test Utility Methods, where it needs to be modified in only one place if it is affected by a change in the SUT.

目标:关注点分离

关注点分离适用于两个维度:(1) 我们希望将测试代码与生产代码分开(将测试逻辑排除在生产代码之外)和 (2) 我们希望每个测试都关注一个关注点(单独测试关注点)以避免模糊测试第 186页)。一个很好的例子就是不要在同一个测试中测试业务逻辑和用户界面,因为这涉及同时测试两个关注点。如果修改了其中一个关注点(例如,用户界面发生变化),则所有测试也需要修改。一次测试一个关注点可能需要将逻辑分离到不同的组件中。这是可测试性设计的一个关键方面,第 11 章使用测试替身”将对此进行进一步探讨。

Separation of Concerns applies in two dimensions: (1) We want to keep test code separate from our production code (Keep Test Logic Out of Production Code) and (2) we want each test to focus on a single concern (Test Concerns Separately) to avoid Obscure Tests (page 186). A good example of what not to do is testing the business logic in the same tests as the user interface, because it involves testing two concerns at the same time. If either concern is modified (e.g., the user interface changes), all the tests would need to be modified as well. Testing one concern at a time may require separating the logic into different components. This is a key aspect of design for testability, a consideration that is explored further in Chapter 11, Using Test Doubles.

随着系统的发展,测试应该需要最少的维护

Tests Should Require Minimal Maintenance as the System Evolves Around Them

变化是生活中的现实。事实上,我们编写自动化测试主要是为了使变化更容易,所以我们应该努力确保我们的测试不会无意中使变化变得更加困难。

Change is a fact of life. Indeed, we write automated tests mostly to make change easier, so we should strive to ensure that our tests don't inadvertently make change more difficult.

假设我们想更改类中某个方法的签名。当我们添加一个新参数时,突然有 50 个测试无法编译。这个结果会促使我们做出改变吗?可能不会。为了解决这个问题,我们引入了一个带有参数的新方法,并安排旧方法调用新方法,将缺失的参数默认为某个值。现在所有测试都编译了,但其中 30 个仍然失败!这些测试有助于我们做出改变吗?

Suppose we want to change the signature of some method on a class. When we add a new parameter, suddenly 50 tests no longer compile. Does that result encourage us to make the change? Probably not. To counter this problem, we introduce a new method with the parameter and arrange to have the old method call the new method, defaulting the missing parameter to some value. Now all of the tests compile but 30 of them still fail! Are the tests helping us make the change?

目标:稳健测试

随着项目的展开和需求的演变,我们不可避免地会对代码进行多种更改。因此,我们希望以这样一种方式编写测试,即任何一次更改所影响的测试数量都非常少。这意味着我们需要尽量减少测试之间的重叠。我们还需要确保测试环境的更改不会影响我们的测试;我们通过尽可能将 SUT 与环境隔离来做到这一点。这会产生更强大的测试

Inevitably, we will want to make many kinds of changes to the code as a project unfolds and its requirements evolve. For this reason, we want to write our tests in such a way that the number of tests affected by any one change is quite small. That means we need to minimize overlap between tests. We also need to ensure that changes to the test environment don't affect our tests; we do this by isolating the SUT from the environment as much as possible. This results in much more Robust Tests.

我们应该努力做到每次测试只验证一个条件。理想情况下,只有一种变更会导致测试需要维护。影响装置设置或拆卸代码的系统变更可以封装在测试实用方法后面,以进一步减少直接受变更影响的测试数量。

We should strive to Verify One Condition per Test. Ideally, only one kind of change should cause a test to require maintenance. System changes that affect fixture setup or teardown code can be encapsulated behind Test Utility Methods to further reduce the number of tests directly affected by the change.

下一步是什么?

What's Next?

本章讨论了我们为什么要进行自动化测试,以及在编写全自动测试时应尝试实现的具体目标。在继续阅读第 5 章“测试自动化原则”之前,我们需要先快速浏览一下第 4 章“测试自动化哲学”,以了解各种测试自动化人员的不同思维方式。

This chapter discussed why we have automated tests and specific goals we should try to achieve when writing Fully Automated Tests. Before moving on to Chapter 5, Principles of Test Automation, we need to take a short side-trip to Chapter 4, Philosophy of Test Automation, to understand the different mindsets of various kinds of test automaters.

第 4 章

测试自动化的哲学

Chapter 4

Philosophy of Test Automation

 

关于本章

About This Chapter

第 3 章测试自动化的目标”描述了实施有效的测试自动化程序的许多目标和好处。本章介绍了人们对设计、构建和测试的看法的不同,这些不同改变了人们自然应用这些模式的方式。“大局”问题包括我们是先写测试还是最后写测试,我们是将它们视为测试还是示例,我们是自内而外构建软件还是自外而内构建软件,我们是验证状态还是行为,以及我们是预先设计装置还是逐个测试。

Chapter 3, Goals of Test Automation, described many of the goals and benefits of having an effective test automation program in place. This chapter introduces some differences in the way people think about design, construction, and testing that change the way they might naturally apply these patterns. The "big picture" questions include whether we write tests first or last, whether we think of them as tests or examples, whether we build the software from the inside-out or from the outside-in, whether we verify state or behavior, and whether we design the fixture upfront or test by test.

为什么哲学很重要?

Why Is Philosophy Important?

哲学与测试自动化有什么关系?关系很大!我们对生活(和测试)的看法强烈影响着我们如何进行自动化测试。当我与 Martin Fowler(该系列的编辑)讨论本书的初稿时,我们得出的结论是,不同的人对基于 xUnit 的测试自动化的处理方式存在哲学差异。这些差异是为什么有些人很少使用Mock Objects第 544页)而其他人却到处使用它们的核心原因。

What's philosophy got to do with test automation? A lot! Our outlook on life (and testing) strongly affects how we go about automating tests. When I was discussing an early draft of this book with Martin Fowler (the series editor), we came to the conclusion that there were philosophical differences between how different people approached xUnit-based test automation. These differences lie at the heart of why, for example, some people use Mock Objects (page 544) sparingly and others use them everywhere.

自从那次令人大开眼界的讨论之后,我一直在寻找测试自动化人员之间的其他哲学差异。这些不同的观点往往是由于有人说“我从来没有(觉得有必要)使用那种模式”或“我从来没有遇到过那种味道”而产生的。通过质疑这些说法,我可以学到很多关于发言者的测试哲学的知识。从这些讨论中得出了以下哲学差异:

Since that eye-opening discussion, I have been on the lookout for other philosophical differences among test automaters. These alternative viewpoints tend to come up as a result of someone saying, "I never (find a need to) use that pattern" or "I never run into that smell." By questioning these statements, I can learn a lot about the testing philosophy of the speaker. Out of these discussions have come the following philosophical differences:

  • “后测试”与“先测试”
  • "Test after" versus "test first"
  • 逐个测试与一次性测试
  • Test-by-test versus test all-at-once
  • “由外而内”与“由内而外”(独立应用于设计和编码)
  • "Outside-in" versus "inside-out" (applies independently to design and coding)
  • 行为验证与状态验证
  • Behavior verification versus state verification
  • “逐个测试设计夹具”与“预先设计大型夹具”
  • "Fixture designed test-by-test" versus "big fixture design upfront"

一些哲学差异

Some Philosophical Differences

先测试还是最后测试?

Test First or Last?

传统软件开发是在设计和编码完所有软件后才准备和执行测试。此步骤顺序适用于客户测试和单元测试。相比之下,敏捷社区已将先编写测试作为标准做法。为什么测试和开发的顺序如此重要?任何尝试将全自动测试(第22页)改造到遗留系统上的人都会告诉你,事后编写自动化测试要困难得多。在软件“已经完成”后,仅仅养成编写自动化单元测试的习惯就已经很具挑战性,无论测试本身是否易于构建。即使我们为可测试性而设计,我们也能轻松自然地编写测试而无需修改生产代码的可能性也很低。但是,如果先编写测试,系统的设计本质上就是可测试的。

Traditional software development prepares and executes tests after all software is designed and coded. This order of steps holds true for both customer tests and unit tests. In contrast, the agile community has made writing the tests first the standard way of doing things. Why is the order in which testing and development take place important? Anyone who has tried to retrofit Fully Automated Tests (page 22) onto a legacy system will tell you how much more difficult it is to write automated tests after the fact. Just having the discipline to write automated unit tests after the software is "already finished" is challenging, whether or not the tests themselves are easy to construct. Even if we design for testability, the likelihood that we can write the tests easily and naturally without modifying the production code is low. When tests are written first, however, the design of the system is inherently testable.

先写测试还有其他一些好处。如果先写测试,并且只写足够的代码让测试通过,生产代码就会更简单。可选的功能往往不会被写出来;不需要花额外的精力去写那些无法正常工作的花哨的错误处理代码。测试往往更健壮,因为根据测试的需要,每个对象只提供必要的方法。

Writing the tests first has some other advantages. When tests are written first and we write only enough code to make the tests pass, the production code tends to be more minimalist. Functionality that is optional tends not to be written; no extra effort goes into fancy error-handling code that doesn't work. The tests tend to be more robust because only the necessary methods are provided on each object based on the tests' needs.

如果软件是“先测试”编写的,那么为了设置夹具和验证结果而访问对象的状态就会更加自然。例如,我们可以完全避免测试异味敏感相等(请参阅第 239页的脆弱测试),因为在断言中使用了对象的正确属性,而不是比较这些对象的字符串表示。我们甚至可能发现我们根本不需要实现表示,因为我们没有真正的需要它。为了验证结果而用测试替身第 522页)替换依赖项的能力也得到了极大增强,因为可替换的依赖项从一开始就设计在软件中。String

Access to the state of the object for the purposes of fixture setup and result verification comes much more naturally if the software is written "test first." For example, we may avoid the test smell Sensitive Equality (see Fragile Test on page 239) entirely because the correct attributes of objects are used in assertions rather than comparing the string representations of those objects. We may even find that we don't need to implement a String representation at all because we have no real need for it. The ability to substitute dependencies with Test Doubles (page 522) for the purpose of verifying the outcome is also greatly enhanced because substitutable dependency is designed into the software from the start.

测试还是例子?

Tests or Examples?

每当我提到在软件编写完成之前为软件编写自动化测试的概念时,一些听众都会露出奇怪的表情。他们会问:“你怎么可能为不存在的软件编写测试?”在这种情况下,我会按照 Brian Marrick 的思路重新组织讨论,讨论“示例”和示例驱动开发 (EDD)。对于某些人来说,示例似乎比“测试”更容易想象在编写代码之前编写。示例是可执行的,并且揭示了需求是否得到满足,这一事实可以留待以后讨论,或者与想象力更丰富的人讨论。

Whenever I mention the concept of writing automated tests for software before the software has been written, some listeners get strange looks on their faces. They ask, "How can you possibly write tests for software that doesn't exist?" In these cases, I follow Brian Marrick's lead by reframing the discussion to talk about "examples" and example-driven development (EDD). It seems that examples are much easier for some people to envision writing before code than are "tests." The fact that the examples are executable and reveal whether the requirements have been satisfied can be left for a later discussion or a discussion with people who have a bit more imagination.

等到您拿到这本书时,EDD 框架系列可能已经出现。基于 Ruby 的RSpec开启了从 TDD 到 EDD 的重新定义,而基于 Java 的JBehave紧随其后。这些“单元测试框架”的基本设计与 xUnit 相同,但术语已发生变化,以反映可执行规范(请参阅第 21页的测试自动化目标)的思维模式。

By the time this book is in your hands, a family of EDD frameworks is likely to have emerged. The Ruby-based RSpec kicked off the reframing of TDD to EDD, and the Java-based JBehave followed shortly thereafter. The basic design of these "unit test frameworks" is the same as xUnit but the terminology has changed to reflect the Executable Specification (see Goals of Test Automation on page 21) mindset.

指定包含业务逻辑的组件的另一种流行替代方法是使用Fit测试。无论我们将编程语言语法做得多么“业务友好”,这些测试对于非技术人员来说总是比用编程语言编写的内容更易读!

Another popular alternative for specifying components that contain business logic is to use Fit tests. These will invariably be more readable by nontechnical people than something written in a programming language regardless of how "business friendly" we make the programming language syntax!

逐个测试还是一次性测试全部?

Test-by-Test or Test All-at-Once?

测试驱动开发过程鼓励我们“编写测试”,然后“编写一些代码”以通过该测试。此过程不是在编写任何代码之前编写所有测试,而是以非常细粒度的方式交错编写测试和代码。“测试一点,编写一点,再测试一点”——这是增量开发的最佳方式。这种方法是唯一的方法吗?一点也不!一些开发人员喜欢在开始任何编码之前确定当前功能所需的所有测试这种策略使他们能够“像客户一样思考”或“像测试人员一样思考”,并让开发人员避免过早陷入“解决方案模式”。

The test-driven development process encourages us to "write a test" and then "write some code" to pass that test. This process isn't a case of all tests being written before any code, but rather the writing of tests and code being interleaved in a very fine-grained way. "Test a bit, code a bit, test a bit more"—this is incremental development at its finest. Is this approach the only way to do things? Not at all! Some developers prefer to identify all tests needed by the current feature before starting any coding. This strategy enables them to "think like a client" or "think like a tester" and lets developers avoid being sucked into "solution mode" too early.

测试驱动的纯粹主义者认为,如果我们一次只进行一个测试,我们就可以更渐进地进行设计。他们说:“如果只有一个测试失败,就更容易集中注意力。”许多测试驱动者报告说,他们很少使用调试器,因为细粒度测试和增量开发几乎不会让人怀疑测试失败的原因;测试提供了缺陷定位(请参阅第22页的测试自动化目标),而我们所做的最后更改(导致问题的更改)仍然记忆犹新。

Test-driven purists argue that we can design more incrementally if we build the software one test at a time. "It's easier to stay focused if only a single test is failing," they say. Many test drivers report not using the debugger very much because the fine-grained testing and incremental development leave little doubt about why tests are failing; the tests provide Defect Localization (see Goals of Test Automation on page 22) while the last change we made (which caused the problem) is still fresh in our minds.

当我们讨论单元测试时,这种考虑尤其重要,因为我们可以选择何时枚举每个对象或方法的详细要求(测试)。一个合理的折衷方案是在任务开始时确定所有单元测试-可能粗略地编写空的测试方法第 348页)框架,但一次只编写一个测试方法主体。我们也可以编写所有测试方法主体,然后禁用除一个测试之外的所有测试,以便我们可以专注于一次一个测试地构建生产代码。

This consideration is especially relevant when we are talking about unit tests because we can choose when to enumerate the detailed requirements (tests) of each object or method. A reasonable compromise is to identify all unit tests at the beginning of a task—possibly roughing in empty Test Method (page 348) skeletons, but coding only a single Test Method body at a time. We could also code all Test Method bodies and then disable all but one of the tests so that we can focus on building the production code one test at a time.

对于客户测试,我们可能不想在用户故事中逐一将测试提供给开发人员。因此,在开始开发单个故事之前,为该故事准备好所有测试是有意义的。有些团队更喜欢在要求他们估计构建故事所需的工作量之前确定故事的客户测试(尽管不一定是完整的),因为测试有助于构建故事。

With customer tests, we probably don't want to feed the tests to the developer one by one within a user story. Therefore, it makes sense to prepare all the tests for a single story before we begin development of that story. Some teams prefer to have the customer tests for the story identified—although not necessarily fleshed out—before they are asked to estimate the effort needed to build the story, because the tests help frame the story.

由外而内还是由内而外?

Outside-In or Inside-Out?

从外向内设计软件意味着我们首先考虑整个系统的黑盒客户测试(也称为故事测试),然后考虑我们设计的每个软件的单元测试。在此过程中,我们还可能对我们决定构建的大粒度组件实施组件测试。

Designing the software from the outside inward implies that we think first about black-box customer tests (also known as storytests) for the entire system and then think about unit tests for each piece of software we design. Along the way, we may also implement component tests for the large-grained components we decide to build.

这些测试集都激励我们在开始像软件开发人员一样思考之前“像客户一样思考”。我们首先关注提供给软件用户的界面,无论该用户是个人还是另一款软件。测试捕获这些使用模式,并帮助我们列举我们需要支持的各种场景。只有当我们确定了所有测试时,我们才“完成”了规范。有些人喜欢从外向内设计,然后从内向外编码,以避免处理“依赖性问题”。这种策略要求在编写内部软件的测试时预测外部软件的需求。这也意味着我们实际上并不孤立地测试外部软件和内部软件。图 4.1说明了这一概念。图中从上到下的进展暗示了我们编写软件的顺序。中下类的测试可以利用它们上方已经构建的类——这种策略避免了许多测试中对测试桩(第 529页) 或模拟对象的需要。在内部组件可能返回特定值或抛出异常但无法按时执行的测试中,我们可能仍需要使用测试桩。在这种情况下,破坏者(参见测试桩)非常有用。

Each of these sets of tests inspires us to "think like the client" well before we start thinking like a software developer. We focus first on the interface provided to the user of the software, whether that user is a person or another piece of software. The tests capture these usage patterns and help us enumerate the various scenarios we need to support. Only when we have identified all the tests are we "finished" with the specification. Some people prefer to design outside-in but then code inside-out to avoid dealing with the "dependency problem." This tactic requires anticipating the needs of the outer software when writing the tests for the inner software. It also means that we don't actually test the outer software in isolation from the inner software. Figure 4.1 illustrates this concept. The top-to-bottom progression in the diagram implies the order in which we write the software. Tests for the middle and lower classes can take advantage of the already-built classes above them—a strategy that avoids the need for Test Stubs (page 529) or Mock Objects in many of the tests. We may still need to use Test Stubs in those tests where the inner components could potentially return specific values or throw exceptions, but cannot be made to do so on cue. In such a case, a Saboteur (see Test Stub) comes in very handy.

图 4.1。 “由内而外”的功能开发。开发从最内层的组件开始,然后基于先前构建的组件继续进行用户界面的开发。

Figure 4.1. "Inside-out" development of functionality. Development starts with the innermost components and proceeds toward the user interface, building on the previously constructed components.

图像

其他测试驱动程序更喜欢从外向内进行设计和编码。从外向内编写代码迫使我们处理“依赖性问题”。我们可以使用测试桩来代替我们尚未编写的软件,以便可以执行和测试软件的外层。我们还可以使用测试桩将“不可能”的间接输入(返回值、out参数或异常)注入 SUT,以验证它是否正确处理这些情况。

Other test drivers prefer to design and code from the outside-in. Writing the code outside-in forces us to deal with the "dependency problem." We can use Test Stubs to stand in for the software we haven't yet written, so that the outer layer of software can be executed and tested. We can also use Test Stubs to inject "impossible" indirect inputs (return values, out parameters, or exceptions) into the SUT to verify that it handles these cases correctly.

图 4.2中,我们颠倒了构建类的顺序。因为下级类还不存在,所以我们用测试替身来代替它们。

In Figure 4.2, we have reversed the order in which we build our classes. Because the subordinate classes don't exist yet, we used Test Doubles to stand in for them.

图 4.2. 测试替身支持的“由外而内”的功能开发。开发从外部开始,使用测试替身代替依赖组件 (DOC),并在确定每个 DOC 的需求后向内进行。

Figure 4.2. "Outside-in" development of functionality supported by Test Doubles. Development starts at the outside using Test Doubles in place of the depended-on components (DOCs) and proceeds inward as requirements for each DOC are identified.

图像

一旦建立了下属类,我们就可以从许多测试中删除测试替身。保留它们可以提供更好的缺陷定位,但代价是潜在的更高测试维护成本。

Once the subordinate classes have been built, we could remove the Test Doubles from many of the tests. Keeping them provides better Defect Localization at the cost of potentially higher test maintenance cost.

状态或行为验证?

State or Behavior Verification?

从外向内编写代码,验证行为而不仅仅是状态只是一小步。“统计”观点认为,将 SUT 置于特定状态、执行它,并在测试结束时验证 SUT 是否处于预期状态就足够了。“行为”观点认为,我们不仅应该指定 SUT 的开始和结束状态,还应该指定 SUT 对其依赖项的调用。也就是说,我们应该指定对 SUT 的“传出接口”的调用的详细信息。SUT 的这些间接输出就像函数返回的值一样,只是我们必须使用特殊措施来捕获它们,因为它们不会直接返回到客户端或测试。

From writing code outside-in, it is but a small step to verifying behavior rather than just state. The "statist" view suggests that it is sufficient to put the SUT into a specific state, exercise it, and verify that the SUT is in the expected state at the end of the test. The "behaviorist" view says that we should specify not only the start and end states of the SUT, but also the calls the SUT makes to its dependencies. That is, we should specify the details of the calls to the "outgoing interfaces" of the SUT. These indirect outputs of the SUT are outputs just like the values returned by functions, except that we must use special measures to trap them because they do not come directly back to the client or test.

行为主义学派有时被称为行为驱动开发。它的证据是在整个测试过程中大量使用模拟对象测试间谍第 538页)。行为验证可以更好地独立测试每个软件单元,尽管这可能会增加重构难度。Martin Fowler 在[MAS]中详细讨论了统计主义和行为主义方法。

The behaviorist school of thought is sometimes called behavior-driven development. It is evidenced by the copious use of Mock Objects or Test Spies (page 538) throughout the tests. Behavior verification does a better job of testing each unit of software in isolation, albeit at a possible cost of more difficult refactoring. Martin Fowler provides a detailed discussion of the statist and behaviorist approaches in [MAS].

提前设计夹具还是逐个测试?

Fixture Design Upfront or Test-by-Test?

在传统测试界,一种流行的方法是定义一个由应用程序和已填充各种测试数据的数据库组成的“测试平台”。数据库的内容经过精心设计,可以执行许多不同的测试场景。

In the traditional test community, a popular approach is to define a "test bed" consisting of the application and a database already populated with a variety of test data. The content of the database is carefully designed to allow many different test scenarios to be exercised.

当以类似的方式处理 xUnit 测试的 Fixture 时,测试自动化程序可以定义一个Standard Fixture (page 305 ),然后将其用于一个或多个Testcase Classes (page 373 ) 的所有测试方法。可以在每个测试方法中使用Delegated Setup ( page 411 )将此 Fixture 设置为Fresh Fixture (page 311 ),或者在使用Implicit Setup (page 424 ) 的方法中将此 Fixture 设置为 Fresh Fixture (page 311)。或者,也可以将其设置为可由许多测试重用的Shared Fixture (page 317 )。无论哪种方式,测试阅读者都可能发现很难确定 Fixture 的哪些部分是特定测试方法的真正先决条件。setUp

When the fixture for xUnit tests is approached in a similar manner, the test automater may define a Standard Fixture (page 305) that is then used for all the Test Methods of one or more Testcase Classes (page 373). This fixture may be set up as a Fresh Fixture (page 311) in each Test Method using Delegated Setup (page 411) or in the setUp method using Implicit Setup (page 424). Alternatively, it can be set up as a Shared Fixture (page 317) that is reused by many tests. Either way, the test reader may find it difficult to determine which parts of the fixture are truly pre-conditions for a particular Test Method.

更敏捷的方法是针对每个测试方法定制设计一个最小夹具第 302页) 。从这个角度来看,没有“前期大型夹具设计”活动。这种方法与使用新鲜夹具最为一致。

The more agile approach is to custom design a Minimal Fixture (page 302) for each Test Method. With this perspective, there is no "big fixture design upfront" activity. This approach is most consistent with using a Fresh Fixture.

当哲学观点不同时

When Philosophies Differ

当然,我们无法总是说服与我们共事的人接受我们的理念。即便如此,了解其他人信奉不同的理念有助于我们理解他们做事方式不同的原因。这并不是说这些人没有与我们相同的目标;1只是他们决定如何使用不同的理念来实现这些目标。了解存在不同的理念并认识到我们信奉哪些理念是找到我们之间共同点的良好第一步。

We cannot always persuade the people we work with to adopt our philosophy, of course. Even so, understanding that others subscribe to a different philosophy helps us appreciate why they do things differently. It's not that these individuals don't share the same goals as ours;1 it's just that they make the decisions about how to achieve those goals using a different philosophy. Understanding that different philosophies exist and recognizing which ones we subscribe to are good first steps toward finding some common ground between us.

我的哲学

My Philosophy

如果你想知道我的个人哲学是什么,下面就是:

In case you were wondering what my personal philosophy is, here it is:

  • 先写测试!
  • Write the tests first!
  • 测试就是例子!
  • Tests are examples!
  • 我通常一次只写一个测试,但有时我会把我能想到的所有测试都列在前面作为骨架。
  • I usually write tests one at a time, but sometimes I list all the tests I can think of as skeletons upfront.
  • 从外向内的开发有助于明确下一层需要进行哪些测试。
  • Outside-in development helps clarify which tests are needed for the next layer inward.
  • 我主要使用状态验证第 462页),但在需要获得良好的代码覆盖率时,也会使用行为验证第 468页)。
  • I use primarily State Verification (page 462) but will resort to Behavior Verification (page 468) when needed to get good code coverage.
  • 我根据每个测试来执行夹具设计。
  • I perform fixture design on a test-by-test basis.

好了!现在你知道来自哪里了。

There! Now you know where I'm coming from.

下一步是什么?

What's Next?

本章介绍了软件设计、构建、测试和测试自动化的基本原理。第 5 章测试自动化原则”介绍了一些关键原则,这些原则将帮助我们实现第 3 章测试自动化目标”中所述的目标。然后,我们将开始研究整体测试自动化策略和各个模式。

This chapter introduced the philosophies that anchor software design, construction, testing, and test automation. Chapter 5, Principles of Test Automation, describes key principles that will help us achieve the goals described in Chapter 3, Goals of Test Automation. We will then be ready to start looking at the overall test automation strategy and the individual patterns.

第 5 章

测试自动化原理

Chapter 5

Principles of Test Automation

 

关于本章

About This Chapter

第 3 章测试自动化的目标”描述了我们应努力实现的目标,以帮助我们成功实现单元测试和客户测试的自动化。第 4 章测试自动化的哲学”讨论了人们在软件设计、构建和测试方面所采用的一些差异。这为经验丰富的测试自动化人员在自动化测试时遵循的原则提供了背景。我将它们称为“原则”有两个原因:它们太高级,不能成为模式,并且它们代表的是一种并非所有人都认同的价值体系。不同的价值体系可能会导致您选择与本书中介绍的模式不同的模式。我希望,明确这一价值体系将加速理解我们分歧之处及其原因的过程。

Chapter 3, Goals of Test Automation, described the goals we should strive to achieve to help us be successful at automating our unit tests and customer tests. Chapter 4, Philosophy of Test Automation, discussed some of the differences in the way people approach software design, construction, and testing. This provides the background for the principles that experienced test automaters follow while automating their tests. I call them "principles" for two reasons: They are too high level to be patterns and they represent a value system that not everyone will share. A different value system may cause you to choose different patterns than the ones presented in this book. Making this value system explicit will, I hope, accelerate the process of understanding where we disagree and why.

原则

The Principles

当 Shaun Smith 和我提出原始《测试自动化宣言》[TAM]中的列表时,我们考虑了是什么促使我们以这种方式编写测试。宣言列出了我们希望在测试中看到的品质— 而不是一组可以直接应用的模式。然而,这些原则使我们确定了一些更具体的原则,其中一些将在本章中描述。这些原则与目标的不同之处在于,人们对它们的争论更多。

When Shaun Smith and I came up with the list in the original Test Automation Manifesto [TAM], we considered what was driving us to write tests the way we did. The Manifesto is a list of the qualities we would like to see in a test—not a set of patterns that can be directly applied. However, those principles have led us to identify a number of somewhat more concrete principles, some of which are described in this chapter. What makes these principles different from the goals is that there is more debate about them.

原则比模式更具“规范性”,本质上也更高级。与模式不同,原则没有替代方案,而是以“这样做是因为”的方式呈现。为了将它们与模式区分开来,我给它们赋予了命令式名称,而不是我为目标、模式和气味使用的名词短语名称。

Principles are more "prescriptive" than patterns and higher level in nature. Unlike patterns, they don't have alternatives, but rather are presented in a "do this because" fashion. To distinguish them from patterns, I have given them imperative names rather than the noun-phrase names I use for goals, patterns, and smells.

在大多数情况下,这些原则同样适用于单元测试和故事测试。可能的例外是“每个测试验证一个条件”原则,这对于执行更多复杂功能块的客户测试可能不切实际。然而,仍然值得努力遵循这些原则,并且只有当您完全意识到后果时才偏离它们。

For the most part, these principles apply equally well to unit tests and storytests. A possible exception is the principle Verify One Condition per Test, which may not be practical for customer tests that exercise more involved chunks of functionality. It is, however, still worth striving to follow these principles and to deviate from them only when you are fully cognizant of the consequences.

原则:先写测试

Principle: Write the Tests First

也称为

Also known as

测试驱动开发、测试优先开发

Test-Driven Development, Test-First Development

测试驱动开发在很大程度上是一种养成的习惯。一旦人们“掌握了它”,以任何其他方式编写代码看起来都会像 TDD 一样奇怪,对于那些从未做过的人来说。支持进行 TDD 的主要理由有两个:

Test-driven development is very much an acquired habit. Once one has "gotten the hang of it," writing code in any other way can seem just as strange as TDD seems to those who have never done it. There are two major arguments in favor of doing TDD:

  1. 单元测试为我们节省了大量的调试工作——这些工作通常完全抵消了自动化测试的成本。
  2. The unit tests save us a lot of debugging effort—effort that often fully offsets the cost of automating the tests.
  3. 在编写代码之前编写测试会强制要求代码具有可测试性。我们不需要将可测试性视为单独的设计条件;它只是因为我们已经编写了测试而发生的。
  4. Writing the tests before we write the code forces the code to be designed for testability. We don't need to think about testability as a separate design condition; it just happens because we have written tests.

原则:可测试性设计

Principle: Design for Testability

考虑到最后一条原则,这条原则可能显得多余。对于选择忽略“先写测试”的开发人员来说,可测试性设计就变成了一条更重要的原则,因为如果没有设计可测试性,他们将无法在事后编写自动化测试。任何尝试将自动化单元测试改造到遗留软件上的人都可以证明这会带来多大的困难。Mike Feathers 在[WEwLC]中讨论了在这种情况下引入测试的特殊技术。

Given the last principle, this principle may seem redundant. For developers who choose to ignore Write the Tests First, Design for Testability becomes an even more important principle because they won't be able to write automated tests after the fact if the testability wasn't designed in. Anyone who has tried to retrofit automated unit tests onto legacy software can testify to the difficulty this raises. Mike Feathers talks about special techniques for introducing tests in this case in [WEwLC].

原则:先使用前门

Principle: Use the Front Door First

也称为

Also known as

前门优先

Front Door First

对象有几种接口。有预期客户端使用的“公共”接口。也可能有只有亲密朋友才能使用的“私有”接口。许多对象还有一个“传出接口”,由它们所依赖的任何对象的接口的使用部分组成。

Objects have several kinds of interfaces. There is the "public" interface that clients are expected to use. There may also be a "private" interface that only close friends should use. Many objects also have an "outgoing interface" consisting of the used part of the interfaces of any objects on which they depend.

我们使用的接口类型会影响测试的稳健性。使用后门操作第 327页)来设置夹具或验证预期结果或测试可能会导致过度耦合的软件(请参阅第 239页的脆弱测试),需要更频繁的测试维护。过度使用行为验证(第468页)和模拟对象(第544页)可能会导致过度指定的软件(请参阅脆弱测试)和更脆弱的测试,并可能阻止开发人员进行理想的重构。

The types of interfaces we use influence the robustness of our tests. The use of Back Door Manipulation (page 327) to set up the fixture or verify the expected outcome or a test can result in Overcoupled Software (see Fragile Test on page 239) that needs more frequent test maintenance. Overuse of Behavior Verification (page 468) and Mock Objects (page 544) can result in Overspecified Software (see Fragile Test) and tests that are more brittle and may discourage developers from doing desirable refactorings.

当所有选择都同样有效时,我们应该使用往返测试来测试我们的 SUT。 为此,我们通过对象的公共接口对其进行测试,并使用状态验证(第462页)来确定其是否行为正确。 如果这不足以准确描述预期行为,我们可以进行跨层测试,并使用行为验证来验证 SUT对依赖组件 (DOC) 的调用。 如果必须用更快的测试替身第 522页)替换缓慢或不可用的 DOC ,则最好使用伪对象第 551页),因为它在测试中编码的假设更少(唯一的假设是伪对象替换的组件确实是需要的)。

When all choices are equally effective, we should use round-trip tests to test our SUT. To do so, we test an object through its public interface and use State Verification (page 462) to determine whether it behaved correctly. If this is not sufficient to accurately describe the expected behavior, we can make our tests layer-crossing tests and use Behavior Verification to verify the calls the SUT makes to depended-on components (DOCs). If we must replace a slow or unavailable DOC with a faster Test Double (page 522), using a Fake Object (page 551) is preferable because it encodes fewer assumptions into the test (the only assumption is that the component that the Fake Object replaces is actually needed).

原则:传达意图

Principle: Communicate Intent

也称为

Also known as

高级语言,一目了然

Higher-Level Language, Single-Glance Readable

完全自动化测试,尤其是脚本测试第 285),都是程序。它们需要在语法上正确才能编译,在语义上正确才能成功运行。它们需要实现将 SUT 置于适当的起始状态并验证是否发生了预期结果所需的任何详细逻辑。虽然这些特征是必要的,但它们还不够,因为它们忽略了测试最重要的解释者:测试维护者

Fully Automated Tests, especially Scripted Tests (page 285), are programs. They need to be syntactically correct to compile and semantically correct to run successfully. They need to implement whatever detailed logic is required to put the SUT into the appropriate starting state and to verify that the expected outcome has occurred. While these characteristics are necessary, they are not sufficient because they neglect the single most important interpreter of the tests: the test maintainer.

包含大量代码1条件测试逻辑第 200页)的测试通常是模糊测试第 186页)。它们更难理解,因为我们需要从所有细节中推断出“大局”。每当我们需要重新访问测试以维护它或将测试用作文档时,这种意义的逆向工程就会花费额外的时间。它还增加了测试的拥有成本并降低了投资回报率。

Tests that contain a lot of code1 or Conditional Test Logic (page 200) are usually Obscure Tests (page 186). They are much harder to understand because we need to infer the "big picture" from all the details. This reverse engineering of meaning takes extra time whenever we need to revisit the test either to maintain it or to use the Tests as Documentation. It also increases the cost of ownership of the tests and reduces their return on investment.

如果我们能够传达意图,测试将更易于理解和维护。我们可以通过使用意图显示名称[SBPP]调用测试实用方法(第 599页)来设置测试装置并验证是否实现了预期结果。在测试方法(第 348页) 中,应该可以很容易地看出测试装置如何影响每个测试的预期结果- 即哪些输入会导致哪些输出。丰富的测试实用方法库也使​​测试更容易编写,因为我们不必将细节编码到每个测试中。

Tests can be made easier to understand and maintain if we Communicate Intent. We can do so by calling Test Utility Methods (page 599) with Intent-Revealing Names [SBPP] to set up our test fixture and to verify that the expected outcome has been realized. It should be readily apparent within the Test Method (page 348) how the test fixture influences the expected outcome of each test—that is, which inputs result in which outputs. A rich library of Test Utility Methods also makes tests easier to write because we don't have to code the details into every test.

原则:不要修改 SUT

Principle: Don't Modify the SUT

有效的测试通常要求我们用测试替身替换应用程序的一部分,或者使用测试特定子类第 579页)覆盖其部分行为。这可能是因为我们需要控制其间接输入,或者因为我们需要通过拦截其间接输出来执行行为验证。也可能是因为应用程序行为的某些部分具有不可接受的副作用或依赖关系,而这些依赖关系在我们的开发或测试环境中是不可能满足的。

Effective testing often requires us to replace a part of the application with a Test Double or override part of its behavior using a Test-Specific Subclass (page 579). This may be because we need to gain control over its indirect inputs or because we need to perform Behavior Verification by intercepting its indirect outputs. It may also be because parts of the application's behavior have unacceptable side effects or dependencies that are impossible to satisfy in our development or test environment.

修改 SUT 是一件危险的事情,无论是我们放入测试钩子第 709页)、覆盖测试特定子类中的行为,还是用测试替身替换 DOC 。在任何这些情况下,我们可能不再真正测试我们计划投入生产的代码。

Modifying the SUT is a dangerous thing whether we are putting in Test Hooks (page 709), overriding behavior in a Test-Specific Subclass, or replacing a DOC with a Test Double. In any of these circumstances, we may no longer actually be testing the code we plan to put into production.

我们需要确保测试软件的配置真正代表了它在生产中的使用方式。如果我们确实需要替换 SUT 所依赖的某些东西以更好地控制 SUT 周围的上下文,我们必须确保以有代表性的方式进行替换。否则,我们最终可能会替换我们认为正在测试的 SUT 的一部分。例如,假设我们正在为对象 X、Y 和 Z 编写测试,其中对象 X 依赖于对象 Y,而对象 Y 又依赖于对象 Z。在为 X 编写测试时,用测试替身替换 Y 和 Z 是合理的在测试 Y 时,我们可以用测试替身替换 Z。但是,在测试 Z 时,我们不能用测试替身替换它,因为 Z 才是我们要测试的!当我们必须重构代码以提高其可测试性时,这种考虑尤为突出。

We need to ensure that we are testing the software in a configuration that is truly representative of how it will be used in production. If we do need to replace something the SUT depends on to get better control of the context surrounding the SUT, we must make sure that we are doing so in a representative way. Otherwise, we may end up replacing part of the SUT that we think we are testing. Suppose, for example, that we are writing tests for objects X, Y, and Z, where object X depends on object Y, which in turn depends on object Z. When writing tests for X, it is reasonable to replace Y and Z with a Test Double. When testing Y, we can replace Z with a Test Double. When testing Z, however, we cannot replace it with a Test Double because Z is what we are testing! This consideration is particularly salient when we have to refactor the code to improve its testability.

当我们使用测试专用子类来覆盖对象的一部分行为以进行测试时,我们必须小心,只覆盖测试需要清除或用于注入间接输入的那些方法。如果我们选择重用为另一个测试创建的测试专用子类,我们必须确保它不会覆盖测试正在验证的任何行为。

When we use a Test-Specific Subclass to override part of the behavior of an object to allow testing, we have to be careful that we override only those methods that the test specifically needs to null out or use to inject indirect inputs. If we choose to reuse a Test-Specific Subclass created for another test, we must ensure that it does not override any of the behavior that this test is verifying.

看待这一原则的另一种方式如下:术语SUT是相对于我们正在编写的测试而言的。在我们的“X 使用 Y 使用 Z”示例中,某些组件测试的 SUT 可能是 X、Y 和 Z 的总和;出于单元测试目的,某些测试可能只是 X,其他测试可能只是 Y,而其他测试可能只是 Z。我们唯一将整个应用程序视为 SUT 的情况是,当我们使用用户界面进行用户验收测试并一路返回数据库时。即使在这里,我们也可能只测试整个应用程序的一个模块(例如,“客户管理模块”)。因此,“SUT”很少等于“应用程序”。

Another way of looking at this principle is as follows: The term SUT is relative to the tests we are writing. In our "X uses Y uses Z" example, the SUT for some component tests might be the aggregate of X, Y, and Z; for unit testing purposes, it might be just X for some tests, just Y for other tests, and just Z for yet other tests. Just about the only time we consider the entire application to be the SUT is when we are doing user acceptance testing using the user interface and going all the way back to the database. Even here, we might be testing only one module of the entire application (e.g., the "Customer Management Module"). Thus "SUT" rarely equals "application."

原则:保持测试独立

Principle: Keep Tests Independent

也称为

Also known as

独立测试

Independent Test

在进行手动测试时,通常的做法是采用较长的测试流程,在一次测试中验证 SUT 行为的许多方面。这种任务聚合是必要的,因为为一次测试设置系统起始状态所涉及的步骤可能只是重复用于验证其行为其他部分的步骤。当手动执行测试时,这种重复并不划算。此外,人工测试人员能够识别何时测试失败应阻止继续执行测试,何时应导致跳过某些测试,或何时失败对后续测试无关紧要(尽管仍可算作失败的测试)。

When doing manual testing, it is common practice to have long test procedures that verify many aspects of the SUT's behavior in a single test. This aggregation of tasks is necessary because the steps involved in setting up the starting state of the system for one test may simply repeat the steps used to verify other parts of its behavior. When tests are executed manually, this repetition is not cost-effective. In addition, human testers have the ability to recognize when a test failure should preclude continuing execution of the test, when it should cause certain tests to be skipped, or when the failure is immaterial to subsequent tests (though it may still count as a failed test.)

如果测试是相互依赖的,甚至(更糟的是)是顺序依赖的,那么我们将无法获得单个测试失败所提供的有用反馈。交互测试(请参阅第228页的不稳定测试)往往会成群失败。将 SUT 置于依赖测试所需状态的测试失败,也会导致依赖测试失败。如果两个测试都失败,我们如何判断失败是反映了两个测试都依赖的代码中存在问题,还是表明只有第一个测试依赖的代码中存在问题?当两个测试都失败时,我们无法判断。而且在这种情况下我们只讨论两个测试——想象一下,如果有数十甚至数百个交互测试,情况会变得多么糟糕。

If tests are interdependent and (even worse) order dependent, we will deprive ourselves of the useful feedback that individual test failures provide. Interacting Tests (see Erratic Test on page 228) tend to fail in a group. The failure of a test that moved the SUT into the state required by the dependent test will lead to the failure of the dependent test, too. With both tests failing, how can we tell whether the failure reflects a problem in code that both tests rely on in some way or whether it signals a problem in code that only the first test relies on? When both tests fail, we can't tell. And we are talking about only two tests in this case—imagine how much worse matters would be with tens or even hundreds of Interacting Tests.

独立测试可以自行运行。它设置自己的Fresh Fixture第 311页),使 SUT 处于可验证其测试行为的状态。构建Fresh Fixture的测试比使用Shared Fixture第 317页)的测试更可能独立。后者可能导致各种不稳定测试,包括单独测试交互测试测试运行战争。使用独立测试时,单元测试失败会为我们提供缺陷定位,帮助我们查明失败的根源。

An Independent Test can be run by itself. It sets up its own Fresh Fixture (page 311) to put the SUT into a state that lets it verify the behavior it is testing. Tests that build a Fresh Fixture are much more likely to be independent than tests that use a Shared Fixture (page 317). The latter can lead to various kinds of Erratic Tests, including Lonely Tests, Interacting Tests, and Test Run Wars. With independent tests, unit test failures give us Defect Localization to help us pinpoint the source of the failure.

原则:隔离 SUT

Principle: Isolate the SUT

有些软件只依赖于(可能正确的)运行时系统或操作系统。大多数软件都建立在我们或他人开发的其他软件之上。当我们的软件依赖于可能随时间变化的其他软件时,我们的测试可能会突然开始失败,因为其他软件的行为已经发生了变化。这个问题称为上下文敏感性(参见脆弱测试),是脆弱测试的一种形式。

Some pieces of software depend on nothing but the (presumably correct) runtime system or operating system. Most pieces of software build on other pieces of software developed by us or by others. When our software depends on other software that may change over time, our tests may suddenly start failing because the behavior of the other software has changed. This problem, which is called Context Sensitivity (see Fragile Test), is a form of Fragile Test.

当我们的软件依赖于我们无法控制其行为的其他软件时,我们可能很难验证我们的软件是否在所有可能的返回值下正常运行。这可能会导致未经测试的代码(请参阅第 268页的生产错误)或未经测试的需求(请参阅生产错误)。为了避免这个问题,我们需要能够在测试的完全控制下将 DOC 的所有可能的反应注入到我们的软件中。

When our software depends on other software whose behavior we cannot control, we may find it difficult to verify that our software behaves properly with all possible return values. This is likely to lead to Untested Code (see Production Bugs on page 268) or Untested Requirements (see Production Bugs). To avoid this problem, we need to be able to inject all possible reactions of the DOC into our software under the complete control of our tests.

无论我们测试的是哪个应用程序、组件、类或方法,我们都应努力将其与我们选择不测试的软件的所有其他部分尽可能地隔离开来。这种元素隔离使我们能够单独测试关注点,并使我们能够保持测试彼此独立。它还有助于我们创建稳健测试,通过降低由于 SUT 与其周围软件之间耦合过多而导致上下文敏感性的可能性。

Whatever application, component, class, or method we are testing, we should strive to isolate it as much as possible from all other parts of the software that we choose not to test. This isolation of elements allows us to Test Concerns Separately and allows us to Keep Tests Independent of one another. It also helps us create a Robust Test by reducing the likelihood of Context Sensitivity caused by too much coupling between our SUT and the software that surrounds it.

我们可以通过设计软件来满足这一原则,这样每个依赖的软件都可以用依赖注入第 678页)或依赖查找(第686页)替换为测试替身,或者用测试特定子类覆盖,从而让我们能够控制 SUT 的间接输入。这种可测试性设计使我们的测试更具可重复性和鲁棒性。

We can satisfy this principle by designing our software such that each piece of depended-on software can be replaced with a Test Double using Dependency Injection (page 678) or Dependency Lookup (page 686) or overridden with a Test-Specific Subclass that gives us control of the indirect inputs of the SUT. This design for testability makes our tests more repeatable and robust.

原则:尽量减少测试重叠

Principle: Minimize Test Overlap

大多数应用程序都有很多功能需要验证。证明所有功能在所有可能的组合和交互场景中都能正常工作几乎是不可能的。因此,选择要编写的测试是一种风险管理练习。

Most applications have lots of functionality to verify. Proving that all of the functionality works correctly in all possible combinations and interaction scenarios is nearly impossible. Therefore, picking the tests to write is an exercise in risk management.

我们应该构建测试,以便尽可能少的测试依赖于特定的功能。这乍一看似乎违反直觉,因为人们会认为我们希望通过尽可能多地测试软件来提高测试覆盖率。不幸的是,验证相同功能的测试通常会同时失败。当 SUT 的功能被修改时,它们也往往需要相同的维护。让多个测试验证相同的功能可能会增加测试维护成本,而且可能不会大大提高质量。

We should structure our tests so that as few tests as possible depend on a particular piece of functionality. This may seem counter-intuitive at first because one would think that we would want to improve test coverage by testing the software as often as possible. Unfortunately, tests that verify the same functionality typically fail at the same time. They also tend to need the same maintenance when the functionality of the SUT is modified. Having several tests verify the same functionality is likely to increase test maintenance costs and probably won't improve quality very much.

我们确实希望确保我们使用的测试涵盖所有测试条件。每个测试条件应仅由一个测试涵盖 — 不多也不少。如果以几种不同的方式测试代码似乎很有价值,我们可能已经确定了几种不同的测试条件。

We do want to ensure that all test conditions are covered by the tests that we do use. Each test condition should be covered by exactly one test—no more, no less. If it seems to provide value to test the code in several different ways, we may have identified several different test conditions.

原则:最小化不可测试的代码

Principle: Minimize Untestable Code

有些类型的代码很难使用全自动测试进行测试。GUI 组件、多线程代码和测试方法立即被认为是“不可测试”的代码。所有这些类型的代码都存在相同的问题:它们嵌入在上下文中,这使得很难通过自动化测试实例化或与它们交互。

Some kinds of code are difficult to test using Fully Automated Tests. GUI components, multithreaded code, and Test Methods immediately spring to mind as "untestable" code. All of these kinds of code share the same problem: They are embedded in a context that makes it hard to instantiate or interact with them from automated tests.

不可测试的代码根本无法进行任何全自动测试,无法防止那些在我们不注意时潜入代码的恶意小错误。这使得安全地重构此代码变得更加困难,并且修改现有功能或引入新功能也更加危险。

Untestable code simply won't have any Fully Automated Tests to protect it from those nefarious little bugs that can creep into code when we aren't looking. That makes it more difficult to refactor this code safely and more dangerous to modify existing functionality or introduce new functionality.

尽量减少必须维护的不可测试代码数量是非常可取的。我们可以重构不可测试代码,通过将要测试的逻辑移出导致可测试性不足的类来提高其可测试性。对于活动对象和多线程代码,我们可以重构为Humble Executable(请参阅第695页的Humble Object)。对于用户界面对象,我们可以重构为Humble Dialog(请参阅Humble Object)。甚至测试方法也可以将其大部分不可测试代码提取到测试实用程序方法中,然后可以对其进行测试。

It is highly desirable to minimize the amount of untestable code that we have to maintain. We can refactor the untestable code to improve its testability by moving the logic we want to test out of the class that is causing the lack of testability. For active objects and multithreaded code, we can refactor to Humble Executable (see Humble Object on page 695). For user interface objects, we can refactor to Humble Dialog (see Humble Object). Even Test Methods can have much of their untestable code extracted into Test Utility Methods, which can then be tested.

当我们最小化不可测试代码时,我们提高了代码的整体测试覆盖率。这样一来,我们还可以提高对代码的信心,并扩展我们随意重构的能力。这种技术可以提高代码质量,这是另一个好处。

When we Minimize Untestable Code, we improve the overall test coverage of our code. In so doing, we also improve our confidence in the code and extend our ability to refactor at will. The fact that this technique improves the quality of the code is yet another benefit.

原则:将测试逻辑排除在生产代码之外

Principle: Keep Test Logic Out of Production Code

也称为

Also known as

生产代码中没有测试逻辑

No Test Logic in Production Code

当生产代码没有针对可测试性进行设计时(无论是由于测试驱动开发还是其他原因),我们可能会倾向于将“钩子”放入生产代码中,以使其更易于测试。这些钩子通常采用以下形式:if  testing  then  ...可以运行替代逻辑或阻止某些逻辑运行。

When the production code hasn't been designed for testability (whether as a result of test-driven development or otherwise), we may be tempted to put "hooks" into the production code to make it easier to test. These hooks typically take the form of if  testing  then  ... and may either run alternative logic or prevent certain logic from running.

测试就是验证系统的行为。如果系统在测试时表现不同,那么我们如何确定生产代码确实有效?更糟糕的是,测试钩子可能会导致软件在生产中失败!

Testing is about verifying the behavior of a system. If the system behaves differently when under test, then how can we be certain that the production code actually works? Even worse, the test hooks could cause the software to fail in production!

生产代码不应包含任何此类条件语句if  testing  then。同样,它也不应包含任何测试逻辑。一个设计良好的系统(从测试角度来看)是允许隔离功能的系统。面向对象的系统特别适合测试,因为它们由离散对象组成。不幸的是,即使是面向对象的系统也可能以难以测试的方式构建,我们仍然可能会遇到嵌入测试逻辑的代码。

The production code should not contain any conditional statements of the if  testing  then sort. Likewise, it should not contain any test logic. A well-designed system (from a testing perspective) is one that allows for the isolation of functionality. Object-oriented systems are particularly amenable to testing because they are composed of discrete objects. Unfortunately, even object-oriented systems can be built in such a way as to be difficult to test, and we may still encounter code with embedded test logic.

原则:每次测试验证一个条件

Principle: Verify One Condition per Test

也称为

Also known as

单一条件测试

Single-Condition Test

许多测试需要除 SUT 的默认状态之外的起始状态,并且 SUT 的许多操作使其处于与原始状态不同的状态。通过将两个测试条件的验证合并到单个测试方法中,人们很容易将一个测试条件的结束状态重新用作下一个测试条件的起始状态,因为这会使测试更有效率。但是,不建议使用这种方法,因为当一个断言失败时,其余测试将不会执行。因此,实现缺陷定位变得更加困难。

Many tests require a starting state other than the default state of the SUT, and many operations of the SUT leave it in a different state from its original state. There is a strong temptation to reuse the end state of one test condition as the starting state of the next test condition by combining the verification of the two test conditions into a single Test Method because this makes testing more efficient. This approach is not recommended, however, because when one assertion fails, the rest of the test will not be executed. As a consequence, it becomes more difficult to achieve Defect Localization.

当我们手动执行测试时,在单个测试中验证多个条件是有意义的,因为测试设置的开销很高,而且实时软件可以适应测试失败。为大量手动测试设置夹具的工作量太大,因此人工测试人员自然倾向于编写冗长的多条件测试。2他们还具有解决遇到的任何问题的能力,因此即使一个步骤失败,也不会一无所获。相比之下,在自动化测试中,一个失败的断言就会导致测试停止运行,其余测试将不会提供有关哪些有效、哪些无效的数据。

Verifying multiple conditions in a single test makes sense when we execute tests manually because of the high overhead of test setup and because the liveware can adapt to test failures. It is too much work to set up the fixture for a large number of manual tests, so human testers naturally tend to write long multiple-condition tests.2 They also have the intelligence to work around any issues they encounter so that all is not lost if a single step fails. In contrast, with automated tests, a single failed assertion will cause the test to stop running and the rest of the test will provide no data on what works and what doesn't.

每个脚本测试都应验证一个测试条件。这种专一性是可能的,因为测试装置是通过编程而不是人工设置的。程序可以非常快速地设置装置,并且它们可以毫无困难地执行数百次完全相同的步骤序列!如果多个测试需要相同的测试装置,我们可以将测试方法移动到每个装置的单个测试用例类中(第 631页),以便我们可以使用隐式设置(第424页),或者我们可以调用测试实用程序方法,使用委托设置第 411页)来设置装置。

Each Scripted Test should verify a single test condition. This single-mindedness is possible because the test fixture is set up programmatically rather than by a human. Programs can set up fixtures very quickly and they don't have trouble executing exactly the same sequence of steps hundreds of times! If several tests need the same test fixture, either we can move the Test Methods into a single Testcase Class per Fixture (page 631) so we can use Implicit Setup (page 424) or we can call Test Utility Methods to set up the fixture using Delegated Setup (page 411).

我们将每个测试设计为有四个不同的阶段(请参阅第 358页的四阶段测试),按顺序执行:固定装置设置、练习 SUT、结果验证固定装置拆卸

We design each test to have four distinct phases (see Four-Phase Test on page 358) that are executed in sequence: fixture setup, exercise SUT, result verification, and fixture teardown.

  • 在第一阶段,我们设置了 SUT 表现出预期行为所需的测试装置(“之前”的图片),以及观察实际结果所需一切(比如使用测试替身)。
  • In the first phase, we set up the test fixture (the "before" picture) that is required for the SUT to exhibit the expected behavior as well as anything we need to put in place to observe the actual outcome (such as using a Test Double).
  • 在第二阶段,我们与 SUT 交互以执行我们试图验证的任何行为。这应该是单一、独特的行为;如果我们尝试执行 SUT 的几个部分,我们就不是在编写单一条件测试
  • In the second phase, we interact with the SUT to exercise whatever behavior we are trying to verify. This should be a single, distinct behavior; if we try to exercise several parts of the SUT, we are not writing a Single-Condition Test.
  • 在第三阶段,我们会尽一切必要的努力来确定是否获得了预期的结果,如果没有,则测试失败。
  • In the third phase, we do whatever is necessary to determine whether the expected outcome has been obtained and fail the test if it has not.
  • 在第四阶段,我们拆除测试装置并将世界恢复到我们发现时的状态。
  • In the fourth phase, we tear down the test fixture and put the world back into the state in which we found it.

请注意,只有一个练习 SUT 阶段和一个结果验证阶段。我们避免使用一系列这样的交替调用(练习、验证、练习、验证),因为这种方法会尝试验证几个不同的条件 — — 最好通过不同的测试方法来处理。

Note that there is a single exercise SUT phase and a single result verification phase. We avoid having a series of such alternating calls (exercise, verify, exercise, verify) because that approach would be trying to verify several distinct conditions—something that is better handled via distinct Test Methods.

“每个测试验证一个条件”的一个可能存在争议的方面是我们所说的“一个条件”是什么意思。一些测试驱动程序坚持每个测试一个断言。这种坚持可能基于使用每个装置组织测试方法的测试用例类,并根据一个断言要验证的内容命名每个测试。3每个测试一个断言使这种命名非常容易,但如果我们必须对许多输出字段进行断言,也会导致更多的测试方法。当然,我们通常可以通过提取自定义断言第 474页)或验证方法(参见自定义断言)来遵守这种解释,这使我们能够将多个断言方法调用减少为一个调用。有时这种方法使测试更具可读性。如果没有,我不会太教条地坚持一个断言。

One possibly contentious aspect of Verify One Condition per Test is what we mean by "one condition." Some test drivers insist on one assertion per test. This insistence may be based on using a Testcase Class per Fixture organization of the Test Methods and naming each test based on what the one assertion is verifying.3 Having one assertion per test makes such naming very easy but also leads to many more test methods if we have to assert on many output fields. Of course, we can often comply with this interpretation by extracting a Custom Assertion (page 474) or Verification Method (see Custom Assertion) that allows us to reduce the multiple assertion method calls to a single call. Sometimes that approach makes the test more readable. When it doesn't, I wouldn't be too dogmatic about insisting on a single assertion.

原则:分别测试关注点

Principle: Test Concerns Separately

复杂应用程序的行为由大量较小行为的集合组成。有时,这些行为中的几个由同一个组件提供。每个行为都是不同的关注点,并且可能有大量需要验证的场景。

The behavior of a complex application consists of the aggregate of a large number of smaller behaviors. Sometimes several of these behaviors are provided by the same component. Each of these behaviors is a different concern and may have a significant number of scenarios in which it needs to be verified.

在单个测试方法中测试多个关注点的问题在于,只要修改任何测试关注点,该方法就会被破坏。更糟糕的是,很难看出哪个关注点有问题。由于缺乏缺陷定位,识别真正的罪魁祸首通常需要手动调试(请参阅第248页的“频繁调试” ) 。最终结果是更多的测试将失败,并且每个测试将花费更长的时间来排除故障和修复。在同一个测试中测试多个关注点也使重构变得更加困难;将急切类“拆分”为几个独立的类(每个类实现一个关注点)将变得更加困难,因为测试需要大量重新设计。

The problem with testing several concerns in a single Test Method is that this method will be broken whenever any of the tested concerns is modified. Even worse, it won't be obvious which concern is the one at fault. Identifying the real culprit typically requires Manual Debugging (see Frequent Debugging on page 248) because of the lack of Defect Localization. The net effect is that more tests will fail and each test will take longer to troubleshoot and fix. Refactoring is also made more difficult by testing several concerns in the same test; it will be harder to "tease apart" the eager class into several independent classes, each of which implements a single concern, because the tests will need extensive redesign.

单独测试我们的关注点可以让故障告诉我们系统的特定部分存在问题,而不是简单地说我们在某个地方存在问题。这种测试方法还使我们更容易理解现在的行为,并在后续重构中分离关注点。也就是说,我们应该能够将测试的子集移动到验证新创建的类的不同测试用例类第 373页);除了更改 SUT 的类名之外,不需要对测试进行太多修改。

Testing our concerns separately allows a failure to tell us that we have a problem in a specific part of our system rather than simply saying that we have a problem somewhere. This approach to testing also makes it easier to understand the behavior now and to separate the concerns in subsequent refactorings. That is, we should just be able to move a subset of the tests to a different Testcase Class (page 373) that verifies the newly created class; it shouldn't be necessary to modify the test much more than changing the class name of the SUT.

原则:确保相应的努力和责任

Principle: Ensure Commensurate Effort and Responsibility

编写或修改测试所需的工作量不应超过实现相应功能所需的工作量。同样,编写或维护测试所需的工具所需的专业知识不应超过实现功能所需的工具所需的专业知识。例如,如果我们可以使用元数据配置 SUT 的行为,并且我们想要编写测试来验证元数据是否设置正确,那么我们不必编写代码来执行此操作。在这种情况下,数据驱动测试(第288页)会更合适。

The amount of effort it takes to write or modify tests should not exceed the effort it takes to implement the corresponding functionality. Likewise, the tools required to write or maintain the test should require no more expertise than the tools used to implement the functionality. For example, if we can configure the behavior of a SUT using metadata and we want to write tests that verify that the metadata is set up correctly, we should not have to write code to do so. A Data-Driven Test (page 288) would be much more appropriate in these circumstances.

下一步是什么?

What's Next?

前几章介绍了测试自动化的常见陷阱(以测试异味的形式)和目标。本章明确了我们在选择模式时使用的价值体系。在第 6 章测试自动化策略”中,我们将研究在项目早期应尝试做出的“难以改变”的决策。

Previous chapters covered the common pitfalls (in the form of test smells) and goals of test automation. This chapter made the value system we use while choosing patterns explicit. In Chapter 6, Test Automation Strategy, we will examine the "hard to change" decisions that we should try to get right early in the project.

第六章

测试自动化策略

Chapter 6

Test Automation Strategy

 

关于本章

About This Chapter

在前面的章节中,我们看到了测试自动化中可能遇到的一些问题。在第 5 章测试自动化原则”中,我们了解了一些可以应用来解决这些问题的原则。本章将更加具体,但仍然侧重于 30,000 英尺的高度。按照逻辑顺序,测试策略先于夹具设置,但这是一个更高级的主题。如果您是使用 xUnit 进行测试自动化的新手,您可能希望跳过本章,在阅读了第 7 章“ xUnit 基础知识”中有关xUnit 基础知识的更多信息以及第 8 章瞬态夹具管理”中有关夹具设置和拆卸的信息后再回来。

In previous chapters, we saw some of the problems we might encounter with test automation. In Chapter 5, Principles of Test Automation, we learned about some of the principles we can apply to help address those problems. This chapter gets a bit more concrete but still focuses at the 30,000-foot level. In the logical sequence of things, test strategy comes before fixture setup but is a somewhat more advanced topic. If you are new to test automation using xUnit, you may want to skip this chapter and come back after reading more about the basics of xUnit in Chapter 7, xUnit Basics, and about fixture setup and teardown in Chapter 8, Transient Fixture Management, and subsequent chapters.

什么是战略?

What's Strategic?

正如前言中的故事充分表明的那样,一开始就很容易走错路。当你缺乏测试自动化经验,并且采用“自下而上”的测试策略时,尤其如此。如果我们能及早发现问题,那么重构测试以消除问题的成本是可以控制的。但是,如果问题长期存在或采用错误的方法来解决问题,那么大量的努力就会白费。这并不是说我们应该遵循“大前期设计”(BDUF)的测试自动化方法。BDUF 几乎总是错误的答案。相反,意识到必要的战略决策并“及时”而不是“太晚”做出这些决策是有帮助的。本章对一些我们需要记住的战略问题进行了“提醒”,以便我们以后不会被它们所蒙蔽。

As the story in the preface amply demonstrates, it is easy to get off on the wrong foot. This is especially true when you lack experience in test automation and when this testing strategy is adopted "bottom up." If we catch the problems early enough, the cost of refactoring the tests to eliminate the problems can be manageable. If, however, the problems are left to fester for too long or the wrong approach is taken to address them, a very large amount of effort can be wasted. This is not to suggest that we should follow a "big design upfront" (BDUF) approach to test automation. BDUF is almost always the wrong answer. Rather, it is helpful to be aware of the strategic decisions necessary and to make them "just in time" rather than "much too late." This chapter gives a "head's up" about some of the strategic issues we want to keep in mind so that we don't get blindsided by them later.

什么使决策具有“战略性”?如果决策“难以改变”,则该决策具有战略性。也就是说,战略决策会影响大量测试,尤其是许多或所有测试都需要同时转换为不同方法时。换句话说,任何可能需要花费大量精力才能改变的决策都是战略性的。

What makes a decision "strategic"? A decision is strategic if it is "hard to change." That is, a strategic decision affects a large number of tests, especially such that many or all the tests would need to be converted to a different approach at the same time. Put another way, any decision that could cost a large amount of effort to change is strategic.

常见的战略决策包括以下考虑因素:

Common strategic decisions include the following considerations:

  • 哪些类型的测试需要自动化?
  • Which kinds of tests to automate?
  • 使用哪些工具来实现自动化?
  • Which tools to use to automate them?
  • 如何管理测试治具?
  • How to manage the test fixture?
  • 如何确保系统易于测试以及测试如何与 SUT 交互?
  • How to ensure that the system is easily tested and how the tests interact with the SUT?

每一个决定都可能产生深远的影响,因此最好是有意识地、在正确的时间、基于现有的最佳信息做出决定。

Each of these decisions can have far-reaching consequences, so they are best made consciously, at the right time, and based on the best available information.

无论我们选择使用哪种测试自动化框架(第 298页),本书中描述的策略和更详细的模式都同样适用。我的大部分经验都来自 xUnit,因此它是本书的重点。但“不要因噎废食”:如果您发现自己使用的是不同类型的测试自动化框架,请记住,您学到的有关 xUnit 的大部分知识可能仍然适用。

The strategies and more detailed patterns described in this book are equally applicable regardless of the kind of Test Automation Framework (page 298) we choose to use. Most of my experience is with xUnit, so it is the focus of this book. But "don't throw out the baby with the bath water": If you find yourself using a different kind of Test Automation Framework, remember that most of what you learn in regard to xUnit may still be applicable.

我们应该实现哪些类型的测试自动化?

Which Kinds of Tests Should We Automate?

粗略地来说,我们可以把测试分为以下两类:

Roughly speaking, we can divide tests into the following two categories:

  • 每个功能测试(也称为功能测试)验证 SUT 对特定刺激的响应行为。
  • Per-functionality tests (also known as functional tests) verify the behavior of the SUT in response to a particular stimulus.
  • 跨功能测试验证了跨特定功能的系统行为的各个方面。
  • Cross-functional tests verify various aspects of the system's behavior that cut across specific functionality.

图 6.1将这两种基本类型的测试显示为两列,每列又进一步细分为更具体的测试类型。

Figure 6.1 shows these two basic kinds of tests as two columns, each of which is further subdivided into more specific kinds of tests.

图 6.1。 我们编写的测试类型及其编写原因的摘要。左栏包含我们编写的测试,这些测试以各种粒度级别描述产品的功能;我们执行这些测试以支持开发。右栏包含涵盖特定功能块的测试;我们执行这些测试以评估产品。每个单元格的底部描述了我们试图传达或验证的内容。

Figure 6.1. A summary of the kinds of tests we write and why. The left column contains the tests we write that describe the functionality of the product at various levels of granularity; we perform these tests to support development. The right column contains tests that span specific chunks of functionality; we execute these tests to critique the product. The bottom of each cell describes what we are trying to communicate or verify.

图像

功能测试

Per-Functionality Tests

功能测试验证软件的直接可观察行为。功能可以与业务相关(例如,系统的主要用例),也可以与操作要求相关(例如,系统维护和特定的容错场景)。大多数这些要求也可以表示为用例、功能、用户故事或测试场景。

Per-functionality tests verify the directly observable behavior of a piece of software. The functionality can be business related (e.g., the principal use cases of the system) or related to operational requirements (e.g., system maintenance and specific fault-tolerance scenarios). Most of these requirements can also be expressed as use cases, features, user stories, or test scenarios.

每个功能测试可以通过功能是否面向业务(或用户)以及其运行的 SUT 的大小来表征。

Per-functionality tests can be characterized by whether the functionality is business (or user) facing and by the size of the SUT on which they operate.

客户测试

客户测试验证整个系统或应用程序的行为。它们通常对应于一个或多个用例、功能或用户故事的场景。这些测试通常有其他名称,例如功能测试、验收测试或最终用户测试。虽然它们可能由开发人员自动执行,但它们的主要特征是,即使用户无法读取测试表示,最终用户也应该能够识别测试指定的行为。

Customer tests verify the behavior of the entire system or application. They typically correspond to scenarios of one or more use cases, features, or user stories. These tests often go by other names such as functional tests, acceptance tests, or end-user tests. Although they may be automated by developers, their key characteristic is that an end user should be able to recognize the behavior specified by the test even if the user cannot read the test representation.

单元测试

单元测试验证单个类或方法的行为,这些行为是设计决策的结果。除非关键业务逻辑块封装在相关类或方法中,否则此行为通常与需求没有直接关系。这些测试由开发人员为自己使用而编写;它们通过以测试的形式总结单元的行为,帮助开发人员描述“完成后的样子”。

Unit tests verify the behavior of a single class or method that is a consequence of a design decision. This behavior is typically not directly related to the requirements except when a key chunk of business logic is encapsulated within the class or method in question. These tests are written by developers for their own use; they help developers describe what "done looks like" by summarizing the behavior of the unit in the form of tests.

组件测试

组件测试验证由一组类组成的组件,这些类共同提供某些服务。就被验证的 SUT 的大小而言,它们介于单元测试和客户测试之间。尽管有些人称这些为“集成测试”或“子系统测试”,但这些术语的含义可能与“整个系统中特定较大粒度子组件的测试”完全不同。

Component tests verify components consisting of groups of classes that collectively provide some service. They fit somewhere between unit tests and customer tests in terms of the size of the SUT being verified. Although some people call these "integration tests" or "subsystem tests," those terms can mean something entirely different from "tests of a specific larger-grained subcomponent of the overall system."

故障插入测试

故障插入测试通常出现在这些功能测试的所有三个粒度级别上,每个级别都会插入不同类型的故障。从测试自动化策略的角度来看,故障插入只是单元和组件测试级别的另一组测试。然而,在整个应用程序级别,事情变得更加有趣。在这里插入故障可能很难实现自动化,因为在不替换应用程序的某些部分的情况下自动插入故障是一项挑战。

Fault insertion tests typically show up at all three levels of granularity within these functional tests, with different kinds of faults being inserted at each level. From a test automation strategy point of view, fault insertion is just another set of tests at the unit and component test levels. Things get more interesting at the whole-application level, however. Inserting faults here can be hard to automate because it is challenging to automate insertion of the faults without replacing parts of the application.

跨功能测试

Cross-Functional Tests

属性测试

性能测试验证系统的各种“非功能性”(也称为“额外功能性”或“跨功能性”)需求。这些需求的不同之处在于它们涵盖各种功能。它们通常对应于架构“功能性”。这些类型的测试包括

Performance tests verify various "nonfunctional" (also known as "extra-functional" or "cross-functional") requirements of the system. These requirements are different in that they span the various kinds of functionality. They often correspond to the architectural "-ilities." These kinds of tests include

  • 响应时间测试
  • Response time tests
  • 容量测试
  • Capacity tests
  • 压力测试
  • Stress tests

从测试自动化的角度来看,许多测试必须实现自动化(至少是部分自动化),因为人工测试人员很难创建足够的负载来验证压力下的行为。虽然我们可以在 xUnit 中连续多次运行相同的测试,但 xUnit 框架并不是特别适合自动化性能测试。

From a test automation perspective, many of these tests must be automated (at least partially) because human testers would have a hard time creating enough load to verify the behavior under stress. While we can run the same test many times in a row in xUnit, the xUnit framework is not particularly well suited to automating performance tests.

敏捷方法的一个优点是,我们可以在项目早期就开始运行这些类型的测试 — 只要架构的关键组件已经粗略确定,并且功能框架可以执行。然后,随着新功能添加到系统框架中,相同的测试可以在整个项目中持续运行。

One advantage of agile methods is that we can start running these kinds of tests quite early in the project—as soon as the key components of the architecture have been roughed in and the skeleton of the functionality is executable. The same tests can then be run continuously throughout the project as new features are added to the system skeleton.

可用性测试

可用性测试通过确认真实用户可以使用软件应用程序实现既定目标来验证“适用性”。这些测试很难实现自动化,因为它们需要人们主观评估使用 SUT 的难易程度。因此,可用性测试很少实现自动化,本书将不再进一步讨论。

Usability tests verify "fitness for purpose" by confirming that real users can use the software application to achieve the stated goals. These tests are very difficult to automate because they require subjective assessment by people regarding how easy it is to use the SUT. For this reason, usability tests are rarely automated and will not be discussed further in this book.

探索性测试

探索性测试是一种确定产品是否自洽的方法。测试人员使用产品,观察其行为,形成假设,设计测试来验证这些假设,并使用这些假设来测试产品。从本质上讲,探索性测试无法自动化,尽管可以使用自动化测试来设置 SUT 以准备进行探索性测试。

Exploratory testing is a way to determine whether the product is self-consistent. The testers use the product, observe how it behaves, form hypotheses, design tests to verify those hypotheses, and exercise the product with them. By its very nature, exploratory testing cannot be automated, although automated tests can be used to set up the SUT in preparation for doing exploratory testing.

我们使用哪些工具来自动化哪些测试?

Which Tools Do We Use to Automate Which Tests?

选择合适的工具与熟练掌握所选工具同样重要。市场上有各种各样的工具,人们很容易被某一特定工具的功能所吸引。工具的选择是一项战略决策:一旦我们投入了大量的时间和精力来学习一种工具并使用该工具自动执行许多测试,更换另一种工具就会变得更加困难。

Choosing the right tool for the job is as important as having good skills with the tools selected for use. A wide array of tools are available in the marketplace, and it is easy to be seduced by the features of a particular tool. The choice of tool is a strategic decision: Once we have invested a lot of time and effort in learning a tool and automating many tests using that tool, it becomes much more difficult to change to a different tool.

有两种完全不同的自动化测试方法(图 6.2)。记录测试第 278页)方法涉及使用工具来监控我们在手动测试 SUT 时与 SUT 的交互。然后将此信息保存到文件或数据库中,并成为针对另一个(甚至是相同)版本的 SUT 重放此测试的脚本。记录测试的主要问题是它们记录的粒度级别。大多数商业工具在用户界面 (UI) 元素级别记录操作,这会导致脆弱测试(第239页)。

There are two fundamentally different approaches to automating tests (Figure 6.2). The Recorded Test (page 278) approach involves the use of tools that monitor our interactions with the SUT while we test it manually. This information is then saved to a file or database and becomes the script for replaying this test against another (or even the same) version of the SUT. The main problem with Recorded Tests is the level of granularity they record. Most commercial tools record actions at the user interface (UI) element level, which results in Fragile Tests (page 239).

图 6.2。 测试自动化选择的三个维度的总结。左侧显示了与 SUT 交互的两种方式。底部边缘列举了我们如何创建测试脚本。从前到后的维度对我们可能选择测试的不同大小的 SUT 进行分类。

Figure 6.2. A summary of the three dimensions of test automation choices. The left side shows the two ways of interacting with the SUT. The bottom edge enumerates how we create the test scripts. The front-to-back dimension categorizes the different sizes of SUT we may choose to test.

图像

第二种自动化测试方法,即手工编写脚本测试(请参阅第 285页的脚本测试),涉及手动编写测试程序(“脚本”),以运行系统。虽然 xUnit 可能是用于准备手工编写脚本测试的最常用的测试自动化框架,但它们也可以通过其他方式准备,包括“批处理”文件、宏语言以及商业或开源测试工具。一些用于准备脚本测试的知名开源工具包括Watir(用 Ruby 编写并在 Internet Explorer 中运行的测试脚本)、Canoo WebTest(用 XML 编写并使用 WebTest 工具运行的测试)以及广受欢迎的Fit(及其基于 wiki 的兄弟FitNesse )。其中一些工具甚至提供测试捕获功能,从而模糊了脚本测试录制测试之间的界限。

The second approach to automating tests, Hand-Scripted Tests (see Scripted Test on page 285) involves the hand-coding of test programs ("scripts") that exercise the system. While xUnit is probably the most commonly used Test Automation Framework for preparing Hand-Scripted Tests, they may be prepared in other ways, including "batch" files, macro languages, and commercial or open-source test tools. Some of the better-known open-source tools for preparing Scripted Tests are Watir (test scripts coded in Ruby and run inside Internet Explorer), Canoo WebTest (tests scripted in XML and run using the WebTest tool), and the ever-popular Fit (and its wiki-based sibling FitNesse). Some of these tools even provide a test capture capability, thereby blurring the lines between Scripted Tests and Recorded Tests.

选择使用哪种测试自动化工具是测试策略决策的重要组成部分。全面介绍可用的各种工具超出了本书的范围,但[ARTRP]中提供了对该主题的更详细讨论。以下各节总结了此处的信息,以概述每种方法的优缺点。

Choosing which test automation tools to use is a large part of the test strategy decision. A full survey of the different kinds of tools available is beyond the scope of this book, but a somewhat more detailed treatment of the topic is available in [ARTRP]. The following sections summarize the information here to provide an overview of the strengths and weaknesses of each approach.

测试自动化的方法和手段

Test Automation Ways and Means

图 6.3将决策可能性描绘成一个矩阵。理论上,这个矩阵中有 2 × 2 × 3 种可能的组合,但通过查看立方体的正面,可以了解这些方法之间的主要区别。四个象限中的一些适用于所有粒度级别;其他一些主要用于自动化客户测试。

Figure 6.3 depicts the decision-making possibilities as a matrix. In theory, there are 2 × 2 × 3 possible combinations in this matrix, but it is possible to understand the primary differences between the approaches by looking at the front face of the cube. Some of the four quadrants are applicable to all levels of granularity; others are primarily used for automating customer tests.

图 6.3. 立方体正面的选择。图 6.2中立方体正面的详细视图,以及每个选项的优点 (+) 和缺点 (-)。

Figure 6.3. The choices on the front face of the cube. A more detailed look at the front face of the cube in Figure 6.2 along with the advantages (+) and disadvantages of each (−).

图像

右上象限:现代 xUnit

立方体正面的右上象限主要由 xUnit 系列测试框架组成。这些框架涉及手动编写脚本的测试,通过内部接口在所有三个粒度级别(系统、组件和单元)上测试系统。一个很好的例子是使用 JUnit 或 NUnit 自动化的单元测试。

The upper-right quadrant of the front face of the cube is dominated by the xUnit family of testing frameworks. These frameworks involve hand-scripting tests that exercise the system at all three levels of granularity (system, component, and unit) via internal interfaces. A good example is unit tests automated using JUnit or NUnit.

右下象限:脚本 UI 测试

此象限代表“现代 xUnit”方法的变体,最常见的示例是使用HttpUnit、JFCUnit、Watir 或类似工具手动编写使用 UI 的测试脚本。也可以使用商业记录测试工具(如QTP)手动编写测试脚本。这些方法都位于右下象限中,处于 SUT 粒度的不同级别。例如,当用于客户测试时,这些工具将在系统测试粒度级别上执行。它们也可以用于仅测试系统的 UI 组件(或甚至可能是某些 UI 单元,例如自定义小部件),尽管这项工作需要在 UI 后面桩实际系统。

This quadrant represents a variation on the "modern xUnit" approach, with the most common examples being the use of HttpUnit, JFCUnit, Watir, or similar tools to hand-script tests using the UI. It is also possible to hand-script tests using commercial Recorded Test tools such as QTP. These approaches all reside within the lower-right quadrant at various levels of SUT granularity. For example, when used for customer tests, these tools would perform at the system test level of granularity. They could also be used to test just the UI component of the system (or possibly even some UI units such as custom widgets), although this effort would require stubbing out the actual system behind the UI.

左下象限:机器人用户

“机器人用户”象限侧重于记录通过 UI 与系统交互的测试。大多数商业测试自动化工具都采用这种方法。它主要适用于“整个系统”粒度,但与脚本 UI 测试一样,如果系统的其余部分可以省略,则可以应用于 UI 组件或单元。

The "robot user" quadrant focuses on recording tests that interact with the system via the UI. Most commercial test automation tools follow this approach. It applies primarily at the "whole system" granularity but, like scripted UI Tests, could be applied to the UI components or units if the rest of the system can be stubbed out.

左上象限:内部记录

为了完整起见,左上象限涉及通过 UI 后面的 API 创建记录测试,方法是记录 SUT 运行时的所有输入和响应。它甚至可能涉及在 SUT(无论我们测试的粒度如何)和任何 DOC 之间插入观察点。在测试回放期间,测试 API 会注入先前记录的输入,并将结果与​​记录的内容进行比较

For completeness, the upper-left quadrant involves creating Recorded Tests via an API somewhere behind the UI by recording all inputs and responses as the SUT is exercised. It may even involve inserting observation points between the SUT (at whatever granularity we are testing) and any DOCs. During test playback, the test APIs inject the inputs recorded earlier and compare the results with what was recorded

这个象限中商业工具1并不多,但在应用程序本身中构建记录测试机制时是一个可行的选择。

This quadrant is not well populated with commercial tools1 but is a feasible option when building a Recorded Test mechanism into the application itself.

介绍 xUnit

Introducing xUnit

xUnit 系列测试自动化框架专为自动化程序员测试而设计。其设计旨在实现以下目标:

The xUnit family of Test Automation Frameworks is designed for use in automating programmer tests. Its design is intended to meet the following goals:

  • 让开发人员可以轻松编写测试,而无需学习新的编程语言。xUnit 适用于当今使用的大多数语言。
  • Make it easy for developers to write tests without needing to learn a new programming language. xUnit is available in most languages in use today.
  • 可以轻松测试单个类和对象,而无需使用应用程序的其余部分。xUnit 旨在让我们从内部测试软件;我们只需设计可测试性即可利用此功能。
  • Make it easy to test individual classes and objects without needing to have the rest of the application available. xUnit is designed to allow us to test the software from the inside; we just have to design for testability to take advantage of this capability.
  • 通过单个操作即可轻松运行一个或多个测试。xUnit 包含测试套件和套件套件的概念(请参阅第387页的测试套件对象)来支持这种测试执行。
  • Make it easy to run one test or many tests with a single action. xUnit includes the concept of a test suite and Suite of Suites (see Test Suite Object on page 387) to support this kind of test execution.
  • 尽量减少运行测试的成本,这样程序员就不会对运行现有测试失去信心。因此,每个测试都应该是一个自我检查测试(第26页),它实现了好莱坞原则。2
  • Minimize the cost of running the tests so programmers aren't discouraged from running the existing tests. For this reason, each test should be a Self-Checking Test (page 26) that implements the Hollywood principle.2

xUnit 家族在实现其目标方面取得了非凡的成功。我无法想象 Erich Gamma 和 Kent Beck 可能预见到 JUnit 的第一个版本会对软件开发产生如此大的影响!3然而,xUnit 的某些特性特别适合于自动化程序员测试,但也可能使其不太适合编写其他类型的测试。特别是,xUnit 中断言的“第一次失败就停止”行为经常受到那些希望使用 xUnit 来自动化多步骤客户测试的人的批评(或推翻),这样他们就可以看到整个结果(哪些有效,哪些无效),而不仅仅是与预期结果的第一次偏差。这种分歧指出了几点:

The xUnit family has been extraordinarily successful at meeting its goals. I cannot imagine that Erich Gamma and Kent Beck could have possibly anticipated just how big an impact that first version of JUnit would have on software development!3 The same characteristics that make xUnit particularly well suited to automating programmer tests, however, may make it less suitable for writing some other kinds of tests. In particular, the "stop on first failure" behavior of assertions in xUnit has often been criticized (or overridden) by people who want to use xUnit for automating multistep customer tests so that they can see the whole score (what worked and what didn't) rather than merely the first deviation from the expected results. This disagreement points out several things:

  • “第一次失败就停止”是一种工具理念,而不是单元测试的特征。大多数测试自动化人员都喜欢让他们的单元测试在第一次失败时停止,而且大多数人都认识到客户测试必须比单元测试更长。
  • "Stop on first failure" is a tool philosophy, not a characteristic of unit tests. It so happens that most test automaters prefer to have their unit tests stop on first failure, and most recognize that customer tests must necessarily be longer than unit tests.
  • 可以改变 xUnit 的基本行为满足特定需求;这种灵活性只是开源工具的优势之一。
  • It is possible to change the fundamental behavior of xUnit to satisfy specific needs; this flexibility is just one advantage of open-source tools.
  • 看到需要改变 xUnit 的基本行为可能应该被解释为考虑其他工具是否可能适合的触发因素。
  • Seeing a need to change the fundamental behavior of xUnit should probably be interpreted as a trigger for considering whether some other tool might possibly be a better fit.

例如,Fit 框架专为运行客户测试而设计。它通过使用颜色编码传达测试每个步骤的通过/失败状态,克服了 xUnit 导致“第一次失败就停止”行为的局限性。Java 开发人员的另一个选择是 TestNG,它提供了显式排序链式测试(第454页)的功能。

For example, the Fit framework has been designed specifically for the purpose of running customer tests. It overcomes the limitations of xUnit that lead to the "stop on first failure" behavior by communicating the pass/fail status of each step of a test using color coding. Another option for Java developers is TestNG, which provides capabilities for explicitly sequencing Chained Tests (page 454).

话虽如此,选择不同的工具并不会消除做出许多战略决策的需要,除非该工具以某种方式限制了这些决策。例如,我们仍然需要为 Fit 测试设置测试装置。某些模式(例如链式测试,其中一个测试为后续测试设置装置)难以自动化,因此在 Fit 中可能不如在 xUnit 中那么有吸引力。具有讽刺意味的是,xUnit 的灵活性却使测试自动化人员陷入如此大的麻烦,他们创建了模糊的测试第 186页),导致高昂的测试维护成本(第265页)?

Having said this, choosing a different tool doesn't eliminate the need to make many of the strategic decisions unless the tool constrains that decision making in some way. For example, we still need to set up the test fixture for a Fit test. Some patterns—such as Chained Tests, where one test sets up the fixture for a subsequent test—are difficult to automate and may therefore be less attractive in Fit than in xUnit. And isn't it ironic that the very flexibility of xUnit is what allows test automaters to get themselves into so much trouble by creating Obscure Tests (page 186) that result in High Test Maintenance Cost (page 265)?

xUnit 的最佳点

The xUnit Sweet Spot

当我们可以将测试组织成一大组小型测试时,xUnit 系列的效果最佳,每个测试都需要一个相对容易设置的小型测试装置。这使我们能够为每个对象的每个测试场景创建单独的测试。应使用Fresh Fixture第 311页)策略管理测试装置,即为每个测试设置一个新的Minimal Fixture第 302页)。

The xUnit family works best when we can organize our tests as a large set of small tests, each of which requires a small test fixture that is relatively easy to set up. This allows us to create a separate test for each test scenario of each object. The test fixture should be managed using a Fresh Fixture (page 311) strategy by setting up a new Minimal Fixture (page 302) for each test.

当我们针对软件 API 编写测试,然后单独测试单个类或小组类时,xUnit 的效果最佳。这种方法使我们能够构建可以快速实例化的小型测试装置。

xUnit works best when we write tests against software APIs and then test single classes or small groups of classes in isolation. This approach allows us to build small test fixtures that can be instantiated quickly.

在进行客户测试时,如果我们定义一种高级语言(第41页) 来描述我们的测试,xUnit 的效果会最好。这种选择将抽象级别提升到更高的水平,远离技术的细节,更接近客户理解的业务概念。从这里开始,将这些测试转换为在 xUnit 或 Fit 中实现的数据驱动测试(第 288页) 只需迈出一小步。

When doing customer tests, xUnit works best if we define a Higher-Level Language (page 41) with which to describe our tests. This choice moves the level of abstraction higher, away from the nitty-gritty of the technology and closer to the business concepts that customers understand. From here, it is a very small step to convert these tests to Data-Driven Tests (page 288) implemented in xUnit or Fit.

请注意,本书中描述的许多高级模式和原则同样适用于 Fit 测试和 xUnit 测试。我还发现它们在使用基于 GUI 的商业测试工具时非常有用,这些工​​具通常使用“记录和回放”隐喻。夹具管理模式在这个领域尤其突出,可重用的“测试组件”也是如此,它们可以串联在一起形成各种测试脚本。这完全类似于 xUnit 的单一用途测试方法(第 348页) 调用可重用的测试实用程序方法(第599页) 以减少它们与 SUT 的 API 的耦合的做法。

Note that many of the higher-level patterns and principles described in this book apply equally well to both Fit tests and xUnit tests. I have also found them to be useful when working with commercial GUI-based testing tools, which typically use a "record and playback" metaphor. The fixture management patterns are particularly salient in this arena, as are reusable "test components" that may be strung together to form a variety of test scripts. This is entirely analogous to the xUnit practice of single-purpose Test Methods (page 348) calling reusable Test Utility Methods (page 599) to reduce their coupling to the SUT's API.

我们使用哪种测试装置策略?

Which Test Fixture Strategy Do We Use?

测试装置管理策略具有战略意义,因为它对测试的执行时间和稳健性有很大影响。选择错误策略的影响不会立即显现,因为至少需要几百个测试才会出现“测试速度慢”(第253页)的异味,并且可能需要几个月的开发才会出现“测试维护成本高”的异味。然而,一旦出现这些异味,就需要改变测试自动化策略,而且由于受影响的测试数量众多,其成本将非常高昂。

The test fixture management strategy is strategic because it has a large impact on the execution time and robustness of the tests. The effects of picking the wrong strategy won't be felt immediately because it takes at least a few hundred tests before the Slow Tests (page 253) smell becomes evident and probably several months of development before the High Test Maintenance Cost smell starts to emerge. Once these smells appear, however, the need to change the test automation strategy will become apparent—and its cost will be significant because of the number of tests affected.

什么是夹具?

What Is a Fixture?

每个测试都由四个部分组成,如四阶段测试第 358页)中所述。在第一阶段,我们创建 SUT 及其依赖的所有内容,并将它们置于执行 SUT 所需的状态。在 xUnit 中,我们将执行 SUT 所需的一切称为测试装置,并将执行的测试逻辑部分称为测试的装置设置阶段。

Every test consists of four parts, as described in Four-Phase Test (page 358). In the first phase, we create the SUT and everything it depends on and put them into the state required to exercise the SUT. In xUnit, we call everything we need in place to exercise the SUT the test fixture, and we call the part of the test logic that we execute to set it up the fixture setup phase of the test.

此时,需要提醒大家注意。术语“fixture”对很多人来说意味着很多东西:

At this point, a word of caution is in order. The term "fixture" means many things to many people:

  • xUnit 的一些变体将 Fixture 的概念与创建它的Testcase Class (第 373页) 分开。JUnit 及其直接端口属于这一类。
  • Some variants of xUnit keep the concept of the fixture separate from the Testcase Class (page 373) that creates it. JUnit and its direct ports fall into this category.
  • xUnit 家族的其他成员假设Testcase Class的实例“是一个”fixture。NUnit 就是一个很好的例子。
  • Other members of the xUnit family assume that an instance of the Testcase Class "is a" fixture. NUnit is a good example.
  • 第三阵营对 Fixture 使用了完全不同的名称。例如,RSpec在包含测试方法的测试上下文类中捕获测试的先决条件(与 NUnit 的想法相同,但术语不同)。
  • A third camp uses an entirely different name for the fixture. For example, RSpec captures the pre-conditions of the test in a test context class that holds the Test Methods (same idea as NUnit but with different terminology).
  • 术语“fixture”在其他类型的测试自动化中含义完全不同。例如,在 Fit 中,它表示我们用来定义高级语言的数据驱动测试解释器[GOF]的定制部分。
  • The term "fixture" is used to mean entirely different things in other kinds of test automation. In Fit, for example, it means the custom-built parts of the Data-Driven Test Interpreter [GOF] that we use to define our Higher-Level Language.

“类‘是’装置”方法假设采用每个装置一个测试用例类(第 631页) 的方法来组织测试。当我们选择不同的测试组织方式时,例如每个类一个测试用例类(第617页) 或每个功能一个测试用例类(第624页),测试装置和测试用例类概念的这种合并可能会造成混淆。在本书中,我使用“测试装置”——或只是“装置”——来表示“测试的先决条件”,使用测试用例类来表示“包含测试方法和设置测试装置所需的任何代码的类”。

The "class 'is a' fixture" approach assumes the Testcase Class per Fixture (page 631) approach to organizing the tests. When we choose a different way of organizing the tests, such as Testcase Class per Class (page 617) or Testcase Class per Feature (page 624), this merging of the concepts of test fixture and Testcase Class can be confusing. Throughout this book, I use "test fixture"—or just "fixture"—to mean "the pre-conditions of the test" and Testcase Class to mean "the class that contains the Test Methods and any code needed to set up the test fixture."

设置夹具的最常见方式是使用前门夹具设置,通过调用 SUT 上的适当方法来构造对象。当 SUT 的状态存储在其他对象或组件中时,我们可以通过将必要的记录直接插入到 SUT 行为所依赖的其他组件中来执行后门设置(请参阅第 327页的后门操作)。我们最常在数据库中使用后门设置,或者在需要使用模拟对象(第544页)或测试替身(第522页)时使用后门设置;这些概念在第 13 章使用数据库测试”第 11 章使用测试替身”中有更详细的介绍。

The most common way to set up the fixture is to use front door fixture setup by calling the appropriate methods on the SUT to construct the objects. When the state of the SUT is stored in other objects or components, we can do Back Door Setup (see Back Door Manipulation on page 327) by inserting the necessary records directly into the other component on which the behavior of the SUT depends. We use Back Door Setup most often with databases or when we need to use a Mock Object (page 544) or Test Double (page 522); these concepts are covered in more detail in Chapter 13, Testing with Databases, and Chapter 11, Using Test Doubles.

主要赛事策略

Major Fixture Strategies

任何事物都有可能有很多种分类方法。为了便于讨论,我们将根据每种策略需要进行的测试开发工作类型对测试装置策略进行分类。

There are probably many ways to classify just about anything. For the purposes of this discussion, we will classify our test fixture strategies based on what kinds of test development work we need to do for each one.

第一个也是最简单的 Fixture 管理策略只需要我们关心如何组织代码来为每个测试构建 Fixture。也就是说,我们是把这段代码放在我们的测试方法中,还是把它放到我们从测试方法中调用的测试实用方法中,还是把它放到我们的测试用例类中的方法中?这个策略涉及使用瞬态新鲜 Fixture(参见新鲜 Fixture)。这些 Fixture 只存在于内存中,一旦我们用完它们,它们就会非常方便地消失。setUp

The first and simplest fixture management strategy requires us to worry only how we will organize the code to build the fixture for each test. That is, do we put this code in our Test Methods, factor it into Test Utility Methods that we call from our Test Methods, or put it into a setUp method in our Testcase Class? This strategy involves the use of Transient Fresh Fixtures (see Fresh Fixture). These fixtures live only in memory and very conveniently disappear as soon as we are done with them.

第二种策略涉及使用Fresh Fixtures,出于某种原因,这些 Fixtures 会持续存在于使用它的单个测试方法之外。为了防止它们变成共享 Fixtures第 317页),这些持久 Fresh Fixtures(请参阅Fresh Fixture)需要显式代码在每次测试结束时将其拆除。此要求使 Fixture 拆除模式发挥作用。

A second strategy involves the use of Fresh Fixtures that, for one reason or another, persist beyond the single Test Method that uses it. To keep them from turning into Shared Fixtures (page 317), these Persistent Fresh Fixtures (see Fresh Fixture) require explicit code to tear them down at the end of each test. This requirement brings into play the fixture teardown patterns.

第三种策略涉及持久性装置,这些装置在许多测试中被有意重复使用。这种共享装置策略通常用于提高使用持久性新鲜装置但带有大量负担的测试的执行速度。这些测试需要使用装置构造和拆卸触发模式之一。它们还涉及彼此交互的测试,无论是设计还是结果,这通常会导致不稳定的测试第 228页)和高昂的测试维护成本

A third strategy involves persistent fixtures that are deliberately reused across many tests. This Shared Fixture strategy is often used to improve the execution speed of tests that use a Persistent Fresh Fixture but comes with a fair amount of baggage. These tests require the use of one of the fixture construction and teardown triggering patterns. They also involve tests that interact with one another, whether by design or by consequence, which often leads to Erratic Tests (page 228) and High Test Maintenance Costs.

表 6.1总结了与三种类型的灯具相关的灯具管理开销。

Table 6.1 summarizes the fixture management overhead associated with each of the three styles of fixtures.

表 6.1. 各种测试夹具策略的夹具设置和拆卸要求摘要

Table 6.1. A Summary of the Fixture Setup and Teardown Requirements of the Various Test Fixture Strategies

图像

注意:共享夹具行假定我们在每次测试运行中都构建一个新的共享夹具,而不是使用预建夹具(第429页)。

Note: The Shared Fixture row assumes we are building a new Shared Fixture each test run rather than using a Prebuilt Fixture (page 429).

图 6.4说明了我们的目标、夹具的新鲜度或夹具重用以及夹具持久性之间的相互作用。它还说明了共享夹具的几个变体。

Figure 6.4 illustrates the interaction between our goals, freshness of fixtures or fixture reuse, and fixture persistence. It also illustrates a few variations of the Shared Fixture.

图 6.4. 主要测试装置策略的总结。新装置可以是临时的,也可以是持久的;共享装置必须是持久的。不可变共享装置(参见共享装置)不得由任何测试修改。因此,大多数测试都会用可以修改的新装置来增强共享装置。

Figure 6.4. A summary of the main test fixture strategies. Fresh Fixtures can be either transient or persistent; Shared Fixtures must be persistent. An Immutable Shared Fixture (see Shared Fixture) must not be modified by any test. As a consequence, most tests augment the Shared Fixture with a Fresh Fixture that they can modify.

图像

对于其中两种组合,持久性和新鲜度之间的关系相当明显。本章后面将更详细地讨论持久性新鲜装置。瞬态共享装置本质上是瞬态的——我们如何保存对这些装置的引用是使它们持久的原因。除了这一区别之外,瞬态共享装置可以与持久共享装置完全一样对待。

The relationship between persistence and freshness is reasonably obvious for two of these combinations. The persistent Fresh Fixture is discussed in more detail later in this chapter. The transient Shared Fixture is inherently transient—how we hold references to these fixtures is what makes them persist. Other than this distinction, transient Shared Fixtures can be treated exactly like persistent Shared Fixtures.

临时新鲜装置

Transient Fresh Fixtures

在这种方法中,每个测试在运行时都会创建一个临时的Fresh Fixture 。它所需的任何对象或记录都由测试本身创建(尽管不一定在测试方法中)。由于测试装置可见性仅限于一个测试,因此我们确保每个测试都是完全独立的,因为它不能(无论是有意还是无意地)依赖于使用相同装置的任何其他测试的输出。

In this approach, each test creates a temporary Fresh Fixture as it runs. Any objects or records it requires are created by the test itself (though not necessarily inside the Test Method). Because the test fixture visibility is restricted to the one test alone, we ensure that each test is completely independent because it cannot depend, either accidentally or on purpose, on the output of any other tests that use the same fixture.

我们将这种方法称为“新鲜夹具”,因为每个测试都从一张白纸开始,然后从那里构建。它不会从其他测试或预构建夹具(第 429页)“继承”或“重用”夹具的任何部分。SUT 使用的每个对象或记录都是“新鲜的”、“全新的”,而不是“以前使用过的”。

We call this approach Fresh Fixture because each test starts with a clean slate and builds from there. It does not "inherit" or "reuse" any part of the fixture from other tests or from a Prebuilt Fixture (page 429). Every object or record used by the SUT is "fresh," "brand new," and not "previously enjoyed."

使用Fresh Fixture方法的主要缺点是需要额外的 CPU 周期来为每个测试创建所有对象。因此,测试的运行速度可能比使用Shared Fixture方法时慢,尤其是在我们使用Persistent Fresh Fixture时。

The main disadvantage of using the Fresh Fixture approach is the additional CPU cycles it takes to create all the objects for each test. As a consequence, the tests may run more slowly than under a Shared Fixture approach, especially if we use a Persistent Fresh Fixture.

持续更新赛事

Persistent Fresh Fixtures

持久的新鲜Fixture听起来有点自相矛盾。我们希望 Fixture 是新鲜的,但它会在单个测试的生命周期之外持续存在!这是什么样的策略?有些人可能会说“愚蠢”,但有时必须这样做。

A Persistent Fresh Fixture sounds a bit oxymoronic. We want the fixture to be fresh, yet it persists beyond the lifetime of a single test! What kind of strategy is that? Some might say "stupid," but sometimes one has to do this.

当我们测试与数据库或其他持久性机制紧密耦合的组件时,我们“被迫”采用这种策略。显而易见的解决方案是,我们不应该让耦合太紧,而是让数据库成为我们正在测试的组件的可替代依赖项。然而,在测试旧版软件时,这一步骤可能并不实用——但我们可能仍然希望享受Fresh Fixture的好处。因此存在持久 Fresh Fixture策略。此策略与瞬态 Fresh Fixture之间的主要区别在于,每次测试后都需要代码来拆除 Fixture。如果 Fixture 的持久性是由使用数据库、文件系统或其他高延迟依赖项引起的,则持久 Fresh Fixture可能会导致测试缓慢。

We are "forced" into this strategy when we are testing components that are tightly coupled to a database or other persistence mechanism. The obvious solution is that we should not let the coupling be so tight, but rather make the database a substitutable dependency of the component we are testing. This step may not be practical when testing legacy software, however—yet we may still want to partake of the benefits of a Fresh Fixture. Hence the existence of the Persistent Fresh Fixture strategy. The key difference between this strategy and the Transient Fresh Fixture is the need for code to tear down the fixture after each test. Persistent Fresh Fixtures can result in Slow Tests if the persistence of the fixture is caused by the use of a database, file system, or other high-latency dependency.

通过应用以下一种或多种模式,我们至少可以部分解决由此产生的慢速测试问题:

We can at least partially address the resulting Slow Tests by applying one or more of the following patterns:

  1. 构建最小固定装置(尽可能最小的固定装置)。
  2. Construct a Minimal Fixture (the smallest fixture possible).
  3. 通过使用测试替身来替换需要花费太长时间设置的数据提供者,从而加快构建速度。
  4. Speed up the construction by using a Test Double to replace the provider of any data that takes too long to set up.
  5. 如果测试仍然不够快,则对任何被引用但未被修改的对象使用不可变共享夹具,以最小化每次需要销毁和重建的夹具部分的大小。
  6. If the tests still are not fast enough, minimize the size of the part of the fixture we need to destroy and reconstruct each time by using an Immutable Shared Fixture for any objects that are referenced but not modified.

我曾合作过的项目团队发现,当我们使用依赖注入第 678页)或依赖查找第 686页)将整个数据库替换为使用一组哈希表而不是表的假数据库(请参阅第 551页的假对象)时,我们的测试运行速度平均提高了 50 倍(是的,它们只花费了 2% 的时间)。每个测试可能需要许多数据库操作来设置和拆除 SUT 中单个查询所需的装置。

The project teams with which I have worked have found that, on average, our tests run 50 times faster (yes, they take 2% as long) when we use Dependency Injection (page 678) or Dependency Lookup (page 686) to replace the entire database with a Fake Database (see Fake Object on page 551) that uses a set of hash tables instead of tables. Each test may require many, many database operations to set up and tear down the fixture required by a single query in the SUT.

最小化测试装置的大小和复杂性有很多好处。最小装置(请参阅最小装置)更容易理解,有助于突出装置与预期结果之间的因果关系。在这方面,它是测试即文档第 23页)的主要推动力。在某些情况下,我们可以通过使用实体链剪切(请参阅第 529页的测试桩)使测试装置变得更小,从而无需实例化我们的测试仅间接依赖的那些对象。这种策略肯定会加快我们测试装置的实例化速度。

There is a lot to be said for minimizing the size and complexity of the test fixture. A Minimal Fixture (see Minimal Fixture) is much easier to understand and helps highlight the cause–effect relationship between the fixture and the expected outcome. In this regard, it is a major enabler of Tests as Documentation (page 23). In some cases, we can make the test fixture much smaller by using Entity Chain Snipping (see Test Stub on page 529) to eliminate the need to instantiate those objects on which our test depends only indirectly. This tactic will certainly speed up the instantiation of our test fixture.

共享装置策略

Shared Fixture Strategies

有时我们不能(或者选择不使用)使用Fresh Fixture策略。在这些情况下,我们可以使用Shared Fixture。在这种方法中,许多测试会重用同一个测试装置实例。

Sometimes we cannot—or choose not to—use a Fresh Fixture strategy. In these cases, we can use a Shared Fixture. In this approach, many tests reuse the same instance of a test fixture.

共享 Fixtures的主要优点是,我们可以节省大量的安装和拆卸 Fixture 的执行时间。其主要缺点是它的一个别名 Stale Fixture 以及描述其最常见副作用的测试异味Interacting Tests(参见Erratic Test)。尽管共享 Fixtures确实有其他好处,但大多数好处可以通过将其他模式应用于Fresh Fixtures来实现;Standard Fixture第 305页)避免了每个测试的 Fixture 设计和编码工作,而无需实际共享 Fixture。

The major advantage of Shared Fixtures is that we save a lot of execution time in setting up and tearing down the fixture. The main disadvantage is conveyed by one of its aliases, Stale Fixture, and by the test smell that describes its most common side effects, Interacting Tests (see Erratic Test). Although Shared Fixtures do have other benefits, most can be realized by applying other patterns to Fresh Fixtures; Standard Fixture (page 305) avoids the fixture design and coding effort for every test without actually sharing the fixture.

现在,如果共享装置如此糟糕,为什么还要讨论它们呢?因为每个人似乎在其职业生涯中至少都会走这条路一次——所以如果您冒险走这条路,我们不妨分享有关它们的最佳可用信息。请注意,这次讨论并不是要鼓励任何人不必要地走这条路,因为这条路铺满了碎玻璃,到处都是毒蛇……好吧,你明白我的意思。

Now, if Shared Fixtures are so bad, why even discuss them? Because everyone seems to go down this road at least once in his or her career—so we might as well share the best available information about them should you venture down that path. Mind you, this discussion isn't meant to encourage anyone to go down this path unnecessarily because it is paved with broken glass, infested with poisonous snakes, and . . . well, you get my drift.

既然我们已决定使用共享装置(我们确实调查了所有可能的替代方案,不是吗?),我们的选择是什么?我们可以进行以下调整(图 6.5):

Given that we have decided to use a Shared Fixture (we did investigate every possible alternative, didn't we?), what are our options? We can make the following adjustments (Figure 6.5):

  • 我们在多大程度上共享一个装置(例如,一个测试用例类,一个测试套件中的所有测试,由特定用户运行的所有测试)
  • How far and wide we share a fixture (e.g., a Testcase Class, all tests in a test suite, all test run by a particular user)
  • 我们多久重新创建一次装置
  • How often we recreate the fixture

图 6.5. 管理共享装置的各种方法。策略按照装置寿命的长度排序,其中寿命最长的装置显示在左侧。

Figure 6.5. The various ways we can manage a Shared Fixture. The strategies are ordered by the length of the fixture's lifetime, with the longestlasting fixture appearing on the left.

图像

共享 Fixture 的测试越多,其中一个测试越有可能把事情搞得一团糟,破坏其后所有测试的一切。我们重建 Fixture 的次数越少,搞乱的 Fixture 的影响就会持续越久。例如,可以在测试运行之外设置预建 Fixture ,从而避免在测试运行过程中设置 Fixture 的全部成本;不幸的是,如果测试没有正确地进行自我清理,也会导致不可重复的测试(参见不稳定的测试)。此策略最常用于使用数据库脚本初始化的数据库沙箱(第650页);一旦 Fixture 损坏,就必须通过重新运行脚本来重新初始化它。如果共享 Fixture可供多个测试运行器第 377页)访问,我们可能会陷入测试运行战争(参见不稳定的测试),其中测试会随机失败,因为它们试图与其他测试同时使用相同的 Fixture 资源。

The more tests that share a fixture, the more likely that one of them will make a mess of things and spoil everything for all the tests that follow it. The less often we reconstruct the fixture, the longer the effects of a messed-up fixture will persist. For example, a Prebuilt Fixture can be set up outside the test run, thereby avoiding the entire cost of setting up the fixture as part of the test run; unfortunately, it can also result in Unrepeatable Tests (see Erratic Test) if tests don't clean up after themselves properly. This strategy is most commonly used with a Database Sandbox (page 650) that is initialized using a database script; once the fixture is corrupted, it must be reinitialized by rerunning the script. If the Shared Fixture is accessible to more than one Test Runner (page 377), we may end up in a Test Run War (see Erratic Test), in which tests fail randomly as they try to use the same fixture resource at the same time as some other test.

通过在每次运行测试套件时设置装置,我们可以避免不可重复的测试测试运行战争。xUnit 提供了几种方法来实现这一点,包括延迟设置第 435页)、套件装置设置第 441页)和设置装饰器(第447页)。大多数面向对象开发人员应该熟悉“延迟初始化”的概念;这里我们只是将该概念应用于测试装置的构造。后两种选择提供了一种在测试运行完成时拆除测试装置的方法,因为它们在适当的时间调用setUp方法和相应的方法;延迟设置没有给我们提供这样做的方法。tearDown

We can avoid both Unrepeatable Tests and Test Run Wars by setting up the fixture each time the test suite is run. xUnit provides several ways to do so, including Lazy Setup (page 435), Suite Fixture Setup (page 441), and Setup Decorator (page 447). The concept of "lazy initialization" should be familiar to most object-oriented developers; here we just apply the concept to the construction of the test fixture. The latter two choices provide a way to tear down the test fixture when the test run is finished because they call a setUp method and a corresponding tearDown at the appropriate times; Lazy Setup does not give us a way to do this.

链式测试是设置共享装置的另一种选择,它涉及按预定义的顺序运行测试,并让每个测试使用前一个测试的结果作为其测试装置。不幸的是,一旦一个测试失败,许多后续测试将提供不稳定的结果,因为它们的先决条件尚未满足。通过让每个测试使用Guard Assertions第 490)来验证其先决条件是否得到满足,可以更容易地诊断此问题。4

Chained Tests represent another option for setting up a Shared Fixture, one that involves running the tests in a predefined order and letting each test use the previous test's results as its test fixture. Unfortunately, once one test fails, many of the tests that follow will provide erratic results because their pre-conditions have not been satisfied. This problem can be made easier to diagnose by having each test use Guard Assertions (page 490) to verify that its pre-conditions have been met.4

如前所述,不可变共享夹具是一种加速使用新鲜夹具的测试的策略。我们还可以使用不可变共享夹具,通过将更改限制在共享夹具较小、可变的部分,使基于共享夹具的测试更稳定。

As mentioned earlier, an Immutable Shared Fixture is a strategy for speeding up tests that use a Fresh Fixture. We can also use an Immutable Shared Fixture to make tests based on a Shared Fixture less erratic by restricting changes to a smaller, mutable part of a Shared Fixture.

我们如何确保可测试性?

How Do We Ensure Testability?

本章涉及的最后一个战略问题是确保可测试性。这里的讨论并非旨在完整处理该主题 — 它太大了,无法在关于测试策略的单个章节中涵盖。尽管如此,我们也不应该掩盖这个问题,因为它肯定会对测试自动化产生重大影响。但首先,我必须站在我的讲台上,简短地讨论一下开发过程。

The last strategic concern touched on in this chapter is ensuring testability. The discussion here isn't intended to be a complete treatment of the topic—it is too large to cover in a single chapter on test strategy. Nevertheless, we shouldn't sweep this issue under the carpet either, because it definitely has a major impact on test automation. But first, I must climb onto my soapbox for a short digression into the development process.

最后测试——后果自负

Test Last—at Your Peril

任何尝试将单元测试改造到现有应用程序上的人可能都经历过很多痛苦!这是我们能做的最难的测试自动化,也是效率最低的。自动化测试的很多好处都来自于软件开发的“调试阶段”,此时此类测试可以减少使用调试工具所花费的时间。第一次尝试自动化单元测试时就对遗留软件进行测试改造是你最不想尝试的事情,因为它肯定会让即使是最坚定的开发人员和项目经理也灰心丧气。

Anyone who has tried to retrofit unit tests onto an existing application has probably experienced a lot of pain! This is the hardest kind of test automation we can do as well as the least productive. A lot of the benefit of automated tests is derived during the "debugging phase" of software development, when such tests can reduce the amount of time spent working with debugging tools. Tackling a test retrofit on legacy software as your first attempt at automated unit testing is the last thing you want to try, as it is sure to discourage even the most determined developers and project managers.

可测试性设计—前期

Design for Testability—Upfront

BDUF 5可测试性设计很难,因为很难知道测试在 SUT 上需要什么样的控制点和观察点。我们很容易构建难以测试的软件。我们还会花费大量时间设计不充分或不必要的可测试性机制。无论哪种方式,我们都将花费大量精力却一无所获。

BDUF5 design for testability is hard because it is difficult to know what the tests will need in the way of control points and observation points on the SUT. We can easily build software that is difficult to test. We can also spend a lot of time designing in testability mechanisms that are either insufficient or unnecessary. Either way, we will have spent a lot of effort with nothing to show for it.

测试驱动的可测试性

Test-Driven Testability

构建由测试驱动的软件的好处在于,我们不必过多考虑可测试性设计;我们只需编写测试,这迫使我们构建可测试性。编写测试的行为定义了 SUT 需要提供的控制点和观察点。一旦我们通过了测试,我们就知道我们有一个可测试的设计。

The nice thing about building our software driven by tests is that we don't have to think very much about design for testability; we just write the tests and that forces us to build for testability. The act of writing the test defines the control points and observation points that the SUT needs to provide. Once we have passed the tests, we know we have a testable design.

现在我已经尽力推广 TDD 作为“可测试性设计”过程,让我们继续讨论如何真正使我们的软件可测试的机制。

Now that I've done my bit promoting TDD as a "design for testability" process, let's get on with our discussion of the mechanics of actually making our software testable.

控制点和观察点

Control Points and Observation Points

测试通过一个或多个接口或交互点与软件6进行交互。从测试的角度来看,这些接口可以充当控制点观察点图 6.6)。

A test interacts with the software6 through one or more interfaces or interaction points. From the test's point of view, these interfaces can act as either control points or observation points (Figure 6.6).

图 6.6。 控制点和观察点。测试通过交互点与 SUT 交互。直接交互点是测试进行的同步方法调用;间接交互点需要某种形式的后门操纵。控制点有指向 SUT 的箭头;观察点有指向 SUT 外的箭头。

Figure 6.6. Control points and observation points. The test interacts with the SUT through interaction points. Direct interaction points are synchronous method calls made by the test; indirect interaction points require some form of Back Door Manipulation. Control points have arrows pointing toward the SUT; observation points have arrows pointing away from the SUT.

图像

控制点是测试要求软件为其执行某些操作的方式。这可能是为了在设置或拆除测试装置时将软件置于特定状态,也可能是为了锻炼 SUT。某些控制点是专门为测试提供的;生产代码不应使用它们,因为它们会绕过输入验证或缩短 SUT 或它所依赖的某些对象的正常生命周期。

A control point is how the test asks the software to do something for it. This could be for the purpose of putting the software into a specific state as part of setting up or tearing down the test fixture, or it could be to exercise the SUT. Some control points are provided strictly for the tests; they should not be used by the production code because they bypass input validation or short-circuit the normal life cycle of the SUT or some object on which it depends.

观察点是测试在测试结果验证阶段发现 SUT 行为的方式。观察点可用于检索 SUT 或 DOC 的测试后状态。它们还可用于监视 SUT 与在执行过程中预计会与之交互的任何组件之间的交互。验证这些间接输出是后门验证的一个例子(请参阅后门操纵)。

An observation point is how the test finds out about the SUT's behavior during the result verification phase of the test. Observation points can be used to retrieve the post-test state of the SUT or a DOC. They can also be used to spy on the interactions between the SUT and any components with which it is expected to interact while it is being exercised. Verifying these indirect outputs is an example of Back Door Verification (see Back Door Manipulation).

控制点和观察点都可以由 SUT 以同步方法调用的形式提供;我们称之为“从前门进入”。一些交互点可能通过“后门”进入 SUT;我们称之为后门操纵。在以下图表中,控制点由指向 SUT 的箭头表示,无论是来自测试还是来自 DOC。观察点由指向测试本身的箭头表示。这些箭头通常从 SUT 或 DOC 7开始,或者从测试开始并与 SUT 或 DOC 交互,然后返回测试。8

Both control points and observation points can be provided by the SUT as synchronous method calls; we call this "going in the front door." Some interaction points may be via a "back door" to the SUT; we call this Back Door Manipulation. In the diagrams that follow, control points are represented by the arrowheads that point to the SUT, whether from the test or from a DOC. Observation points are represented by the arrows whose heads point back to the test itself. These arrows typically start at the SUT or DOC7 or start at the test and interact with either the SUT or DOC before returning to the test.8

交互风格和可测试性模式

Interaction Styles and Testability Patterns

当测试特定的软件时,我们的测试可以采用两种基本形式之一。

When testing a particular piece of software, our tests can take one of two basic forms.

往返测试仅通过其公共接口(即其“前门”)与相关 SUT 交互(图 6.7)。典型往返测试中的控制点和观察点都是简单的方法调用。这种方法的好处在于它不违反封装。测试只需要知道软件的公共接口;它不需要知道软件是如何构建的。

A round-trip test interacts with the SUT in question only through its public interface—that is, its "front door" (Figure 6.7). Both the control points and the observation points in a typical round-trip test are simple method calls. The nice thing about this approach is that it does not violate encapsulation. The test needs to know only the public interface of the software; it doesn't need to know anything about how it is built.

图 6.7。 往返测试仅通过前门与 SUT 交互。右侧的测试用伪对象替换 DOC,以提高其可重复性或性能。

Figure 6.7. A round-trip test interacts with the SUT only via the front door. The test on the right replaces a DOC with a Fake Object to improve its repeatability or performance.

图像

主要的替代方法是跨层测试图 6.8),其中我们通过 API 执行 SUT,并使用某种形式的测试替身(例如测试间谍第 538页)或模拟对象)密切关注后门出现的情况。这是一种非常强大的测试技术,可用于验证某些类型(主要是架构要求)。不幸的是,如果过度使用这种方法,也会导致过度指定软件(参见脆弱测试),因为软件实现其职责的方式发生变化可能会导致测试失败。

The main alternative is the layer-crossing test (Figure 6.8), in which we exercise the SUT through the API and keep an eye on what comes out the back door using some form of Test Double such as a Test Spy (page 538) or Mock Object. This can be a very powerful testing technique for verifying certain kinds of mostly architectural requirements. Unfortunately, this approach can also result in Overspecified Software (see Fragile Test) if it is overused because changes in how the software implements its responsibilities can cause tests to fail.

图 6.8。 跨层测试可以通过“后门”与 SUT 交互。左侧的测试使用测试桩控制 SUT 的间接输入。右侧的测试使用模拟对象验证其间接输出。

Figure 6.8. A layer-crossing test can interact with the SUT via a "back door." The test on the left controls the SUT's indirect inputs using a Test Stub. The test on the right verifies its indirect outputs using a Mock Object.

图像

图 6.8中,左侧的测试使用测试桩代替 DOC 作为控制点。右侧的测试使用模拟对象代替 DOC 作为观察点。这种风格的测试意味着分层架构 [DDD、PEAA、WWW],这反过来又为使用层测试(第337页)独立测试架构的每一层打开了大门(图 6.9)。更通用的概念是使用组件测试(参见层测试)独立测试层内的每个组件。

In Figure 6.8, the test on the left uses a Test Stub that stands in for the DOC as a control point. The test on the right uses a Mock Object that stands in for the DOC as the observation point. Testing in this style implies a Layered Architecture [DDD, PEAA, WWW], which in turn opens the door to using Layer Tests (page 337) to test each layer of the architecture independently (Figure 6.9). An even more general concept is the use of Component Tests (see Layer Test) to test each component within a layer independently.

图 6.9。 一对层测试,每个层测试系统的不同层。分层架构的每一层都可以使用一组不同的层测试进行独立测试。这确保了关注点的良好分离,并且测试强化了分层架构。

Figure 6.9. A pair of Layer Tests each testing a different layer of the system. Each layer of a layered architecture can be tested independently using a distinct set of Layer Tests. This ensures good separation of concerns, and the tests reinforce the layered architecture.

图像

每当我们想要编写跨层测试时,我们需要确保已经为 SUT 所依赖但我们想要独立测试的任何组件构建了可替代的依赖机制。领先的竞争者包括依赖注入的任何变体(图 6.10)或某种形式的依赖查找,如对象工厂服务定位器。这些依赖替换机制可以手工编码,或者我们可以使用控制反转 (IOC) 框架(如果我们的编程环境中可用)。后备计划是使用SUT 或相关 DOC 的测试特定子类第 579页)。此子类可用于覆盖 SUT 中的依赖访问或构造机制,或将 DOC 的行为替换为测试特定行为。

Whenever we want to write layer-crossing tests, we need to ensure that we have built in a substitutable dependency mechanism for any components on which the SUT depends but that we want to test independently. The leading contenders include any of the variations of Dependency Injection (Figure 6.10) or some form of Dependency Lookup such as Object Factory or Service Locator. These dependency substitution mechanisms can be hand-coded or we can use an inversion of control (IOC) framework if one is available in our programming environment. The fallback plan is to use a Test-Specific Subclass (page 579) of the SUT or the DOC in question. This subclass can be used to override the dependency access or construction mechanism within the SUT or to replace the behavior of the DOC with test-specific behavior.

图 6.10。 测试将测试替身“注入”到 SUT 中。测试可以使用依赖注入将 DOC 替换为适当的测试替身。测试在创建 DOC 时或创建之后将其传递给 SUT。

Figure 6.10. A Test Double being "injected" into a SUT by a test. A test can use Dependency Injection to replace a DOC with an appropriate Test Double. The DOC is passed to the SUT by the test as or after it has been created.

图像

“最后的解决办法”是测试钩子第 709页)。9这些构造确实具有作为临时措施的实用性,使我们能够在重构以提高可测试性的同时,将测试自动化,充当安全网(第24页)。然而,我们绝对不应该养成使用它们的习惯,因为继续使用测试钩子会导致生产中的测试逻辑(第 217页)。

The "solution of last resort" is the Test Hook (page 709).9 These constructs do have utility as temporary measures that allow us to automate tests to act as a Safety Net (page 24) while refactoring to retrofit testability. We definitely shouldn't make a habit of using them, however, as continued use of Test Hooks will result in Test Logic in Production (page 217).

值得一提的第三种测试是异步测试,其中测试通过实际消息传递与 SUT 交互。由于对这些请求的响应也是异步的,因此这些测试必须包括某种进程间同步,例如对 的调用wait。不幸的是,需要等待可能永远不会到达的消息响应可能会导致这些测试花费更长的时间才能执行。在单元和组件测试中应不惜一切代价避免这种测试风格。

A third kind of test worth mentioning is the asynchronous test, in which the test interacts with the SUT through real messaging. Because the responses to these requests also come asynchronously, these tests must include some kind of interprocess synchronization such as calls to wait. Unfortunately, the need to wait for message responses that might never arrive can cause these tests to take much, much longer to execute. This style of testing should be avoided at all costs in unit and component tests.

幸运的是,Humble Executable模式(请参见第695页的Humble Object)可以消除以这种方式进行单元测试的需要(图 6.11)。它涉及将处理传入消息的逻辑放入单独的类或组件中,然后可以使用往返或跨层样式对其进行同步测试。

Fortunately, the Humble Executable pattern (see Humble Object on page 695) can remove the need to conduct unit tests this way (Figure 6.11). It involves putting the logic that handles the incoming message into a separate class or component, which can then be tested synchronously using either a round-trip or layer-crossing style.

图 6.11。Humble Executable 使测试更容易。Humble Executable 模式可以提高验证逻辑的可重复性和速度,否则必须通过异步测试进行验证。

Figure 6.11. A Humble Executable making testing easier. The Humble Executable pattern can improve the repeatability and speed of verifying logic that would otherwise have to be verified via asynchronous tests.

图像

一个相关的问题是通过 UI 测试业务逻辑。一般来说,这种间接测试(参见模糊测试)不是一个好主意,因为对 UI 代码的更改会破坏试图验证其背后业务逻辑的测试。由于 UI 往往会频繁更改,尤其是在敏捷项目中,这种策略将大大增加测试维护成本。这不是一个好主意的另一个原因是 UI 本质上是异步的。通过 UI 运行系统的测试必须是异步测试,以及随之而来的所有问题。

A related issue is the testing of business logic through a UI. In general, such Indirect Testing (see Obscure Test) is a bad idea because changes to the UI code will break tests that are trying to verify the business logic behind it. Because the UI tends to change frequently, especially on agile projects, this strategy will greatly increase test maintenance costs. Another reason this is a bad idea is that UIs are inherently asynchronous. Tests that exercise the system through the UI have to be asynchronous tests along with all the issues that come with them.

划分并测试

Divide and Test

只要我们有足够的测试来确保不会在重构过程中引入错误,我们就可以通过重构将几乎任何难以测试的代码第 209页)转变为易于测试的代码。

We can turn almost any Hard-to-Test Code (page 209) into easily tested code through refactoring as long as we have enough tests in place to ensure that we do not introduce bugs during this refactoring.

我们可以避免将 UI 用于客户测试,方法是将这些测试编写为皮下测试(请参阅分层测试)。这些测试绕过系统的 UI 层,并通过 Service Facade [CJ2EEP]执行业务逻辑,该服务门面向测试公开必要的同步交互点。UI 依赖于相同的门面,使我们能够在连接 UI 逻辑之前验证业务逻辑是否正常工作。分层架构还使我们能够在业务逻辑完成之前测试 UI 逻辑;我们可以用测试替身替换服务门面,该替身提供我们的测试可以依赖的完全确定性的行为。10

We can avoid using the UI for customer tests by writing those tests as Subcutaneous Tests (see Layer Test). These tests bypass the UI layer of the system and exercise the business logic via a Service Facade [CJ2EEP] that exposes the necessary synchronous interaction points to the test. The UI relies on the same facade, enabling us to verify that the business logic works correctly even before we hook up the UI logic. The layered architecture also enables us to test the UI logic before the business logic is finished; we can replace the Service Facade with a Test Double that provides completely deterministic behavior that our tests can depend on.10

在对非平凡 UI 进行单元测试时,11我们可以使用Humble Dialog(参见Humble Object)将做出有关 UI 决策的逻辑从难以同步测试的可视层移出,并移至支持对象层,这可以通过标准单元测试技术进行验证(图 6.12)。这种方法允许对表示逻辑行为进行与业务逻辑行为一样彻底的测试。

When conducting unit testing of nontrivial UIs,11 we can use a Humble Dialog (see Humble Object) to move the logic that makes decisions about the UI out of the visual layer, which is difficult to test synchronously, and into a layer of supporting objects, which can be verified with standard unit-testing techniques (Figure 6.12). This approach allows the presentation logic behavior to be tested as thoroughly as the business logic behavior.

图 6.12。Humble Dialog 减少了测试对 UI 框架的依赖。控制 UI 组件状态的逻辑可能非常难以测试。将其提取到可测试组件中,留下了一个几乎不需要测试的 Humble Dialog。

Figure 6.12. A Humble Dialog reducing the dependency of the test on the UI framework. The logic that controls the state of UI components can be very difficult to test. Extracting it into a testable component leaves behind a Humble Dialog that requires very little testing.

图像

从测试自动化策略的角度来看,关键是要决定应该使用哪种测试-SUT交互风格以及应该避免哪种风格,并确保软件设计能够支持该决定。

From a test automation strategy perspective, the key thing is to make the decision about which test–SUT interaction styles should be used and which ones should be avoided, and to ensure that the software is designed to support that decision.

下一步是什么?

What's Next?

至此,我们对确定测试自动化策略时必须做出的难以改变的决定的介绍就结束了。鉴于您仍在阅读,我将假设您已决定 xUnit 是进行测试自动化的合适工具。以下章节介绍了实现我们选择的夹具策略的详细模式,无论它涉及新鲜夹具还是共享夹具。首先,我们将在第 8 章瞬态夹具管理”中探索最简单的情况,即瞬态新鲜夹具。然后,我们将在第 9 章持久夹具管理”中研究持久夹具的使用。但首先,我们必须在第7 章“xUnit 基础知识”中建立本书使用的基本的 xUnit 术语和符号。

This concludes our introduction to the hard-to-change decisions we must make as we settle upon our test automation strategy. Given that you are still reading, I will assume that you have decided xUnit is an appropriate tool for doing your test automation. The following chapters introduce the detailed patterns for implementing our chosen fixture strategy, whether it involves a Fresh Fixture or a Shared Fixture. First, we will explore the simplest case, a Transient Fresh Fixture, in Chapter 8, Transient Fixture Management. We will then investigate the use of persistent fixtures in Chapter 9, Persistent Fixture Management. But first, we must establish the basic xUnit terminology and notation that is used throughout this book in Chapter 7, xUnit Basics.

第 7 章

xUnit 基础

Chapter 7

xUnit Basics

 

关于本章

About This Chapter

第 6 章测试自动化策略”介绍了我们在项目早期需要做出的“难以改变”的决策。本章有两个目的。首先,它介绍了本书中使用的 xUnit 术语和图表符号。其次,它解释了 xUnit 框架在幕后是如何运作的,以及为什么以这种方式构建它。这些知识可以帮助新测试自动化框架(第 298页) 的构建者了解如何移植 xUnit。它还可以帮助测试自动化人员了解如何使用 xUnit 的某些功能。

Chapter 6, Test Automation Strategy, introduced the "hard to change" decisions that we need to get right early in the project. The current chapter serves two purposes. First, it introduces the xUnit terminology and diagramming notation used throughout this book. Second, it explains how the xUnit framework operates beneath the covers and why it was built that way. This knowledge can help the builder of a new Test Automation Framework (page 298) understand how to port xUnit. It can also help test automaters understand how to use certain features of xUnit.

xUnit 简介

An Introduction to xUnit

术语xUnit是我们用来指代用于自动化手动编写脚本测试(请参阅第285页的脚本测试)的测试自动化框架系列的任何成员,这些框架具有这里描述的一组通用功能。当今广泛使用的大多数编程语言都至少有一种 xUnit 实现;手动编写脚本测试通常使用与用于构建 SUT 的相同编程语言进行自动化。虽然情况不一定如此,但这种策略通常要容易得多,因为我们的测试可以轻松访问 SUT API。通过使用开发人员熟悉的编程语言,学习如何自动化全自动化测试第 26页)所需的工作量较少。1

The term xUnit is how we refer to any member of the family of Test Automation Frameworks used for automating Hand-Scripted Tests (see Scripted Test on page 285) that share the common set of features described here. Most programming languages in widespread use today have at least one implementation of xUnit; Hand-Scripted Tests are usually automated using the same programming language as is used for building the SUT. Although this is not necessarily the case, this strategy is usually much easier because our tests have easy access to the SUT API. By using a programming language with which the developers are familiar, less effort is required to learn how to automate Fully Automated Tests (page 26).1

共同特征

Common Features

鉴于 xUnit 家族的大多数成员都是使用面向对象编程语言(OOPL)实现的,因此这里首先对它们进行描述,然后指出该家族非 OOPL 成员的不同之处。

Given that most members of the xUnit family are implemented using an object-oriented programming language (OOPL), they are described here first and then places where the non-OOPL members of the family differ are noted.

xUnit 系列的所有成员都实现了一组基本功能。它们都提供了执行以下任务的方法:

All members of the xUnit family implement a basic set of features. They all provide a way to perform the following tasks:

  • 将测试指定为测试方法(第 348页)
  • Specify a test as a Test Method (page 348)
  • 以调用断言方法的形式在测试方法中指定预期结果(第 362页)
  • Specify the expected results within the test method in the form of calls to Assertion Methods (page 362)
  • 将测试聚合到可以作为单个操作运行的测试套件中
  • Aggregate the tests into test suites that can be run as a single operation
  • 运行一个或多个测试以获取测试运行结果的报告
  • Run one or more tests to get a report on the results of the test run

由于 xUnit 系列的许多成员都支持测试方法发现(请参阅第 393页的测试发现,因此我们不必在这些成员中使用测试枚举第 399页)来手动将要运行的每个测试方法添加到测试套件中。一些成员还支持某种形式的测试选择第 403页),以便根据某些标准运行测试方法的子集。

Because many members of the xUnit family support Test Method Discovery (see Test Discovery on page 393), we do not have to use Test Enumeration (page 399) in these members to manually add each Test Method we want to run to a test suite. Some members also support some form of Test Selection (page 403) to run subsets of test methods based on some criteria.

最低限度

The Bare Minimum

以下是我们需要了解的有关 xUnit 如何运行的最低限度(图 7.1):

Here is the bare minimum we need to understand about how xUnit operates (Figure 7.1):

  • 我们如何运行测试
  • How we run the tests
  • 我们如何解读测试结果
  • How we interpret the test results

图 7.1。 测试自动化人员看到的静态测试结构。测试自动化人员在读取或编写测试时只能看到静态结构。测试自动化人员为测试用例类中的每个测试编写一个包含四个不同阶段的测试方法。测试套件工厂(参见测试枚举)仅用于测试枚举。运行时结构(显示为灰色)留给测试自动化人员去想象。

Figure 7.1. The static test structure as seen by a test automater. The test automater sees only the static structure as he or she reads or writes tests. The test automater writes one Test Method with four distinct phases for each test in the Testcase Class. The Test Suite Factory (see Test Enumeration) is used only for Test Enumeration. The runtime structure (shown grayed out) is left to the test automater's imagination.

图像

定义测试

Defining Tests

每个测试都由一个测试方法表示,该方法通过以下步骤实施单个四阶段测试(第 358页):

Each test is represented by a Test Method that implements a single Four-Phase Test (page 358) by following these steps:

最常见的测试类型是简单成功测试(请参阅测试方法它验证 SUT 在有效输入下是否正确运行,以及预期异常测试(请参阅测试方法它验证 SUT 在不正确使用时是否会引发异常。一种特殊的测试类型,构造函数测试(请参阅测试方法),验证对象构造函数逻辑是否正确构建新对象。构造函数测试的“简单成功”和“预期异常”形式可能都是需要的。包含我们的测试逻辑的测试方法需要存在于某个地方,因此我们将它们定义为测试用例类的方法。3然后,我们将此测试用例类的名称(或其所在的模块或程序集)传递给测试运行器第 377页)以运行我们的测试。这可以明确完成(例如在命令行上调用测试运行器时),也可以由我们正在使用的集成开发环境 (IDE) 隐式完成。

The most common types of tests are the Simple Success Test (see Test Method), which verifies that the SUT has behaved correctly with valid inputs, and the Expected Exception Test (see Test Method), which verifies that the SUT raises an exception when used incorrectly. A special type of test, the Constructor Test (see Test Method), verifies that the object constructor logic builds new objects correctly. Both "simple success" and "expected exception" forms of the Constructor Test may be needed. The Test Methods that contain our test logic need to live somewhere, so we define them as methods of a Testcase Class.3 We then pass the name of this Testcase Class (or the module or assembly in which it resides) to the Test Runner (page 377) to run our tests. This may be done explicitly—such as when invoking the Test Runner on a command line—or implicitly by the integrated development environment (IDE) that we are using.

什么是 Fixture?

What's a Fixture?

测试装置是我们测试 SUT 所需的一切。通常,它至少包含我们正在测试其方法的类的实例。它还可能包含 SUT 所依赖的其他对象。请注意,xUnit 家族的一些成员将测试用例称为测试装置 — 这种偏好可能反映了一种假设,即测试用例类上的所有测试方法都应使用相同的装置。这种不幸的名称冲突使讨论测试装置变得特别困难。在本书中,我对测试用例类和它创建的测试装置使用了不同的名称。我相信读者会将此术语翻译成他或她的 xUnit 家族特定成员的术语。

The test fixture is everything we need to have in place to exercise the SUT. Typically, it includes at least an instance of the class whose method we are testing. It may also include other objects on which the SUT depends. Note that some members of the xUnit family call the Testcase Class the test fixture—a preference that likely reflects an assumption that all Test Methods on the Testcase Class should use the same fixture. This unfortunate name collision makes discussing test fixtures particularly problematic. In this book, I have used different names for the Testcase Class and the test fixture it creates. I trust that the reader will translate this terminology to the terminology of his or her particular member of the xUnit family.

定义测试套件

Defining Suites of Tests

大多数测试运行器会“自动”构建一个包含测试用例类中所有测试方法的测试套件。通常,这就是我们所需要的。有时我们想运行整个应用程序的所有测试;有时我们只想运行那些专注于特定功能子集的测试。xUnit 系列的一些成员和一些第三方工具实现了测试用例类发现(请参阅测试发现),其中测试运行器通过在文件系统或可执行文件中搜索测试套件来查找测试套件。

Most Test Runners "auto-magically" construct a test suite containing all of the Test Methods in the Testcase Class. Often, this is all we need. Sometimes we want to run all the tests for an entire application; at other times we want to run just those tests that focus on a specific subset of the functionality. Some members of the xUnit family and some third-party tools implement Testcase Class Discovery (see Test Discovery) in which the Test Runner finds the test suites by searching either the file system or an executable for test suites.

如果我们不具备此功能,则需要使用测试套件枚举(请参阅测试枚举其中我们将整个系统或应用程序的总体测试套件定义为几个较小测试套件的集合。为此,我们必须定义一个特殊的测试套件工厂类,其suite方法返回一个测试套件对象,其中包含要运行的测试方法和其他测试套件对象的集合。

If we do not have this capability, we need to use Test Suite Enumeration (see Test Enumeration), in which we define the overall test suite for the entire system or application as an aggregate of several smaller test suites. To do so, we must define a special Test Suite Factory class whose suite method returns a Test Suite Object containing the collection of Test Methods and other Test Suite Objects to run.

这种将测试套件集合成越来越大的套件的方法是将某个类的单元测试套件纳入包或模块的测试套件中,而后者又包括在整个系统的测试套件中。这种分层组织支持以不同程度的完整性运行测试套件,并为开发人员提供了一种实用的方法来运行与感兴趣的软件最相关的测试子集。它还允许他们在将更改提交到源代码存储库 [SCM] 之前使用单个命令运行所有存在的测试。

This collection of test suites into increasingly larger Suites of Suites is commonly used as a way to include the unit test suite for a class into the test suite for the package or module, which is in turn included in the test suite for the entire system. Such a hierarchical organization supports the running of test suites with varying degrees of completeness and provides a practical way for developers to run that subset of the tests that is most relevant to the software of interest. It also allows them to run all the tests that exist with a single command before they commit their changes into the source code repository [SCM].

运行测试

Running Tests

测试通过Test Runner运行。xUnit系列的大多数成员都有几种不同类型的Test Runner 。

Tests are run by using a Test Runner. Several different kinds of Test Runners are available for most members of the xUnit family.

图形测试运行器(参见测试运行器)为用户提供了一种可视化的方式来指定、调用和观察运行测试套件的结果。一些图形测试运行器允许用户通过输入测试套件工厂的名称来指定测试;其他图形测试运行器提供图形测试树资源管理器(参见测试运行器),可用于从测试套件树中选择要执行的特定测试方法,其中测试方法充当树的叶子。许多图形测试运行器都集成到 IDE 中,使运行测试变得像Run  As  Test从上下文菜单中选择命令一样简单。

A Graphical Test Runner (see Test Runner) provides a visual way for the user to specify, invoke, and observe the results of running a test suite. Some Graphical Test Runners allow the user to specify a test by typing in the name of a Test Suite Factory; others provide a graphical Test Tree Explorer (see Test Runner) that can be used to select a specific Test Method to execute from within a tree of test suites, where the Test Methods serve as the tree's leaves. Many Graphical Test Runners are integrated into an IDE to make running tests as easy as selecting the Run  As  Test command from a context menu.

命令行测试运行器(参见测试运行器)可用于在从命令行运行测试套件时执行测试,如图7.2所示。用于创建测试套件的测试套件工厂的名称作为命令行参数包含在内。命令行测试运行器最常用于从集成构建 [SCM] 脚本调用测试运行器,有时也用于从 IDE 中调用测试运行器。

A Command-Line Test Runner (see Test Runner) can be used to execute tests when running the test suite from the command line, as in Figure 7.2. The name of the Test Suite Factory that should be used to create the test suite is included as a command-line parameter. Command-Line Test Runners are most commonly used when invoking the Test Runner from Integration Build [SCM] scripts or sometimes from within an IDE.

图 7.2. 使用命令行测试运行器从命令行运行测试。

Figure 7.2. Using a Command-Line Test Runner to run tests from the command line.

>ruby testrunner.rb c:/examples/tests/SmellHandlerTest.rb

已加载套件 SmellHandlerTest

已开始

.....

0.016 秒内完成。5

个测试,6 个断言,0 个失败,0 个错误

>退出代码:0

>ruby  testrunner.rb  c:/examples/tests/SmellHandlerTest.rb

Loaded  suite  SmellHandlerTest

Started

.....

Finished  in  0.016  seconds.

5  tests,  6  assertions,  0  failures,  0  errors

>Exit  code:  0

 

测试结果

Test Results

当然,运行自动化测试的主要原因是确定结果。为了使结果有意义,我们需要一种标准的方式来描述它们。一般来说,xUnit 家族的成员都遵循好莱坞原则(“不要给我们打电话,我们会给你打电话”)。换句话说,“没有消息就是好消息”;当出现问题时,测试会“给你打电话”。因此,我们可以专注于测试失败,而不是检查一堆通过的测试。

Naturally, the main reason for running automated tests is to determine the results. For the results to be meaningful, we need a standard way to describe them. In general, members of the xUnit family follow the Hollywood principle ("Don't call us; we'll call you"). In other words, "No news is good news"; the tests will "call you" when a problem occurs. Thus we can focus on the test failures rather than inspecting a bunch of passing tests as they roll by.

测试结果分为三类,每类的处理方式略有不同。如果测试运行时没有出现任何错误或失败,则认为测试成功。通常,xUnit 不会对成功的测试执行任何特殊操作 — 当自检测试(第26页)通过时,无需检查任何输出。

Test results are classified into one of three categories, each of which is treated slightly differently. When a test runs without any errors or failures, it is considered to be successful. In general, xUnit does not do anything special for successful tests—there should be no need to examine any output when a Self-Checking Test (page 26) passes.

当断言失败时,测试被视为失败。也就是说,测试通过调用断言方法断言某事应该为真,但结果却并非如此。当它失败时,断言方法会抛出断言失败异常(或编程语言支持的任何类似异常)。测试自动化框架会为每次失败增加一个计数器,并将失败详细信息添加到失败列表中;测试运行完成后,可以稍后更仔细地检查此列表。单个测试的失败虽然很重要,但不会阻止其余测试的运行;这符合保持测试独立的原则(参见第42页)。

A test is considered to have failed when an assertion fails. That is, the test asserts that something should be true by calling an Assertion Method, but that assertion turns out not to be the case. When it fails, an Assertion Method throws an assertion failure exception (or whatever facsimile the programming language supports). The Test Automation Framework increments a counter for each failure and adds the failure details to a list of failures; this list can be examined more closely later, after the test run is complete. The failure of a single test, while significant, does not prevent the remaining tests from being run; this is in keeping with the principle Keep Tests Independent (see page 42).

当 SUT 或测试本身以意外方式失败时,测试被视为有错误。根据所使用的语言,此问题可能由未捕获的异常、引发的错误或其他内容组成。与断言失败一样,测试自动化框架会为每个错误增加一个计数器,并将错误详细信息添加到错误列表中,然后可以在测试运行完成后检查这些错误。

A test is considered to have an error when either the SUT or the test itself fails in an unexpected way. Depending on the language being used, this problem could consist of an uncaught exception, a raised error, or something else. As with assertion failures, the Test Automation Framework increments a counter for each error and adds the error details to a list of errors, which can then be examined after the test run is complete.

对于每个测试错误或测试失败,xUnit 都会记录可供检查的信息,以帮助准确了解问题所在。至少,会记录测试方法测试用例类的名称,以及问题的性质(无论是失败的断言还是软件错误)。在大多数与 IDE 集成的图形测试运行器中,只需(双击)回溯中的相应行即可查看发出失败或导致错误的源代码。

For each test error or test failure, xUnit records information that can be examined to help understand exactly what went wrong. As a minimum, the name of the Test Method and Testcase Class are recorded, along with the nature of the problem (whether it was a failed assertion or a software error). In most Graphical Test Runners that are integrated with an IDE, one merely has to (double-) click on the appropriate line in the traceback to see the source code that emitted the failure or caused the error.

由于测试错误这个名字听起来比测试失败更严重,所以一些测试自动化程序会尝试捕获 SUT 引发的所有错误并将其转变为测试失败。这完全没有必要。具有讽刺意味的是,在大多数情况下,确定测试错误的原因比确定测试失败的原因更容易:测试错误的堆栈跟踪通常会精确定位 SUT 中的问题代码,而测试失败的堆栈跟踪仅仅显示测试中做出失败断言的位置。但是,值得使用Guard Assertions第 490页)来避免在测试方法中执行会导致从测试方法4本身内部引发测试错误的代码;这只是验证执行 SUT 的预期结果的正常部分,并不会删除有用的诊断回溯。

Because the name test error sounds more drastic than a test failure, some test automaters try to catch all errors raised by the SUT and turn them into test failures. This is simply unnecessary. Ironically, in most cases it is easier to determine the cause of a test error than the cause of a test failure: The stack trace for a test error will typically pinpoint the problem code within the SUT, whereas the stack track for a test failure merely shows the location in the test where the failed assertion was made. It is, however, worthwhile using Guard Assertions (page 490) to avoid executing code within the Test Method that would result in a test error being raised from within the Test Method4 itself; this is just a normal part of verifying the expected outcome of exercising the SUT and does not remove useful diagnostic tracebacks.

在 xUnit 封面下

Under the xUnit Covers

到目前为止,描述主要集中在测试方法测试用例类上,偶尔会提到测试套件。这种简化的“编译时”视图足以让大多数人开始在 xUnit 中编写自动化单元测试。无需进一步了解测试自动化框架的运作方式,就可以使用 xUnit——但缺乏更广泛的知识可能会导致在构建和重用测试装置时产生混淆。因此,最好了解 xUnit 实际上如何运行测试方法。在xUnit 家族的大多数5 个成员中,每个测试方法在运行时都由一个测试用例对象(第 382页)表示,因为如果它们是“一流”对象 (图 7.3 ),则操作测试会容易得多。测试用例对象被聚合到测试套件对象中,然后可以使用这些对象通过单个用户操作运行许多测试。

The description thus far has focused on Test Methods and Testcase Classes with the odd mention of test suites. This simplified "compile time" view is enough for most people to get started writing automated unit tests in xUnit. It is possible to use xUnit without any further understanding of how the Test Automation Framework operates—but the lack of more extensive knowledge is likely to lead to confusion when building and reusing test fixtures. Thus it is better to understand how xUnit actually runs the Test Methods. In most5 members of the xUnit family, each Test Method is represented at runtime by a Testcase Object (page 382) because it is a lot easier to manipulate tests if they are "first-class" objects (Figure 7.3). The Testcase Objects are aggregated into Test Suite Objects, which can then be used to run many tests with a single user action.

图 7.3。 测试自动化框架所见的运行时测试结构。在运行时,测试运行器要求测试用例类或测试套件工厂为每个测试方法实例化一个测试用例对象,这些对象被包装在单个测试套件对象中。测试运行器告诉这个复合[GOF]对象运行其测试并收集结果。每个测试用例对象运行一个测试方法。

Figure 7.3. The runtime test structure as seen by the Test Automation Framework. At runtime, the Test Runner asks the Testcase Class or a Test Suite Factory to instantiate one Testcase Object for each Test Method, with the objects being wrapped up in a single Test Suite Object. The Test Runner tells this Composite [GOF] object to run its tests and collect the results. Each Testcase Object runs one Test Method.

图像

测试命令

Test Commands

测试运行器不可能知道如何单独调用每个测试方法。为了避免这种需要,xUnit 系列的大多数成员将每个测试方法转换为带有方法的命令[GOF]run对象。为了创建这些测试用例对象测试运行器调用测试用例类suite的方法来获取测试套件对象。然后,它通过标准测试接口调用该方法。测试用例对象的方法执行它实例化的特定测试方法并报告它是通过还是失败。测试套件对象的方法遍历测试集合的所有成员,跟踪运行了多少个测试以及哪些测试失败了。runrunrun

The Test Runner cannot possibly know how to call each Test Method individually. To avoid the need for this, most members of the xUnit family convert each Test Method into a Command [GOF] object with a run method. To create these Testcase Objects, the Test Runner calls the suite method of the Testcase Class to get a Test Suite Object. It then calls the run method via the standard test interface. The run method of a Testcase Object executes the specific Test Method for which it was instantiated and reports whether it passed or failed. The run method of a Test Suite Object iterates over all the members of the collection of tests, keeping track of how many tests were run and which ones failed.

测试套件对象

Test Suite Objects

测试套件对象是一个复合对象,它实现所有测试用例对象都实现的相同标准测试接口。该接口(在缺少类型或接口构造的语言中是隐式的)需要提供一种run方法。预期是,当调用时,接收器中包含的run所有测试都将运行。对于测试用例对象,它本身就是一个“测试”,并将运行相应的测试方法。对于测试套件对象,这意味着调用它包含的所有测试用例对象。使用复合命令的价值在于它将运行一个测试和运行多个测试的过程转变为完全相同的过程。run

A Test Suite Object is a Composite object that implements the same standard test interface that all Testcase Objects implement. That interface (implicit in languages lacking a type or interface construct) requires provision of a run method. The expectation is that when run is invoked, all of the tests contained in the receiver will be run. In the case of a Testcase Object, it is itself a "test" and will run the corresponding Test Method. In the case of a Test Suite Object, that means invoking run on all of the Testcase Objects it contains. The value of using a Composite Command is that it turns the processes of running one test and running many tests into exactly the same process.

到目前为止,我们假设已经实例化了测试套件对象。但是它是从哪里来的呢?按照惯例,每个测试用例类都充当一个测试套件工厂测试套件工厂提供了一个名为的类方法suite,该方法返回一个测试套件对象,其中包含类中每个测试方法的一个测试用例对象。在支持某种形式反射的语言中,xUnit 可以使用测试方法发现来发现测试方法并自动构造包含它们的测试套件对象。xUnit 系列的其他成员要求测试自动化程序自己实现方法;这种测试枚举需要更多的精力,也更容易导致测试丢失(请参阅第268页的生产错误)。suite

To this point, we have assumed that we already have the Test Suite Object instantiated. But where did it come from? By convention, each Testcase Class acts as a Test Suite Factory. The Test Suite Factory provides a class method called suite that returns a Test Suite Object containing one Testcase Object for each Test Method in the class. In languages that support some form of reflection, xUnit may use Test Method Discovery to discover the test methods and automatically construct the Test Suite Object containing them. Other members of the xUnit family require test automaters to implement the suite method themselves; this kind of Test Enumeration takes more effort and is more likely to lead to Lost Tests (see Production Bugs on page 268).

程序世界中的 xUnit

xUnit in the Procedural World

测试自动化框架和测试驱动开发只有在面向对象编程普及之后才变得流行起来。xUnit 家族的大多数成员都是用支持测试用例对象概念的面向对象编程语言实现的。虽然缺少对象不应该阻止我们测试程序代码,但它确实使编写自检测试更加费力,构建通用、可重用的测试运行器也更加困难。

Test Automation Frameworks and test-driven development became popular only after object-oriented programming became commonplace. Most members of the xUnit family are implemented in object-oriented programming languages that support the concept of a Testcase Object. Although the lack of objects should not keep us from testing procedural code, it does make writing Self-Checking Tests more labor-intensive and building generic, reusable Test Runners more difficult.

在没有对象或类的情况下,我们必须将测试方法视为全局(公共静态)过程。这些方法通常存储在文件或模块中(或语言支持的任何模块化机制)。如果语言支持过程变量(也称为函数指针)的概念,我们可以定义一个通用的测试套件过程(参见测试套件对象),它将测试方法数组(通常称为“测试过程”)作为参数。通常,必须使用测试枚举将测试方法聚合到数组中,因为很少有非面向对象的编程语言支持反射。

In the absence of objects or classes, we must treat Test Methods as global (public static) procedures. These methods are typically stored in files or modules (or whatever modularity mechanism the language supports). If the language supports the concept of procedure variables (also known as function pointers), we can define a generic Test Suite Procedure (see Test Suite Object) that takes an array of Test Methods (commonly called "test procedures") as an argument. Typically, the Test Methods must be aggregated into the arrays using Test Enumeration because very few non-object-oriented programming languages support reflection.

如果语言不支持将测试方法视为数据的任何方式,我们必须通过编写明确调用测试方法和/或其他测试套件程序的测试套件程序来定义测试套件。可以通过在模块上定义方法来启动测试运行。main

If the language does not support any way of treating Test Methods as data, we must define the test suites by writing Test Suite Procedures that make explicit calls to Test Methods and/or other Test Suite Procedures. Test runs may be initiated by defining a main method on the module.

最后一种选择是将测试编码为文件中的数据,并使用单个数据驱动测试第 288页)解释器来执行它们。这种方法的主要缺点是它将可以运行的测试类型限制为由数据驱动测试解释器实现的测试类型,而数据驱动测试解释器本身必须为每个 SUT 重新编写。这种策略的优点是将实际测试的编码从开发人员领域转移到最终用户或测试人员领域,这使得它特别适合客户测试。

A final option is to encode the tests as data in a file and use a single Data-Driven Test (page 288) interpreter to execute them. The main disadvantage of this approach is that it restricts the kinds of tests that can be run to those implemented by the Data-Driven Test interpreter, which must itself be written anew for each SUT. This strategy does have the advantage of moving the coding of the actual tests out of the developer arena and into the end-user or tester arena, which makes it particularly appropriate for customer tests.

下一步是什么?

What's Next?

在本章中,我们建立了讨论如何组合 xUnit 测试的基本术语。现在我们将注意力转向一项新任务 — 在第 8 章“瞬态夹具管理”中构建我们的第一个测试夹具

In this chapter we established the basic terminology for talking about how xUnit tests are put together. Now we turn our attention to a new task—constructing our first test fixture in Chapter 8, Transient Fixture Management.

第 8 章

瞬态夹具管理

Chapter 8

Transient Fixture Management

 

关于本章

About This Chapter

第 6 章测试自动化策略”讨论了我们需要做出的战略决策。其中包括“fixture”一词的定义和测试fixture策略的选择。第7 章“xUnit 基础”介绍了 xUnit 的基本术语和图表符号。本章以前面两章为基础,重点介绍了所选fixture策略的实现机制。

Chapter 6, Test Automation Strategy, looked at the strategic decisions that we need to make. That included the definition of the term "fixture" and the selection of a test fixture strategy. Chapter 7, xUnit Basics, established our basic xUnit terminology and diagramming notation. This chapter builds on both of these earlier chapters by focusing on the mechanics of implementing the chosen fixture strategy.

有几种不同的方法来设置Fresh Fixture (第 311页),我们的决定将影响编写测试所需的工作量、维护测试所需的工作量以及我们是否能将测试作为文档(参见第 23页)。持久 Fresh Fixtures (参见Fresh Fixture ) 的设置方式与瞬态 Fresh Fixtures (参见Fresh Fixture )相同尽管需要考虑一些与 Fixture 拆卸相关的其他因素 (图 8.1 )。共享 Fixtures (第317页) 引入了另一组注意事项。第 9 章将详细讨论持久 Fresh Fixtures共享 Fixtures

There are several different ways to set up a Fresh Fixture (page 311), and our decision will affect how much effort it takes to write the tests, how much effort it takes to maintain our tests, and whether we achieve Tests as Documentation (see page 23). Persistent Fresh Fixtures (see Fresh Fixture) are set up the same way as Transient Fresh Fixtures (see Fresh Fixture), albeit with some additional factors to consider related to fixture teardown (Figure 8.1). Shared Fixtures (page 317) introduce another set of considerations. Persistent Fresh Fixtures and Shared Fixtures are discussed in detail in Chapter 9.

图 8.1. 瞬态新鲜夹具。新鲜夹具有两种类型:瞬态和持久。两者都需要夹具设置;后者还需要夹具拆卸。

Figure 8.1. Transient Fresh Fixture. Fresh Fixtures come in two flavors: Transient and Persistent. Both require fixture setup; the latter also requires fixture teardown.

图像

测试夹具术语

Test Fixture Terminology

在我们讨论设置装置之前,我们需要同意装置是什么。

Before we can talk about setting up a fixture, we need to agree what a fixture is.

什么是夹具?

What Is a Fixture?

每个测试都由四个部分组成,如四阶段测试第 358页)中所述。第一部分是我们创建 SUT 及其依赖的所有内容,并将这些元素置于执行 SUT 所需的状态。在 xUnit 中,我们将执行 SUT 所需的一切称为测试装置,将执行设置 SUT 的测试逻辑部分称为装置设置

Every test consists of four parts, as described in Four-Phase Test (page 358). The first part is where we create the SUT and everything it depends on and where we put those elements into the state required to exercise the SUT. In xUnit, we call everything we need in place to exercise the SUT the test fixture and the part of the test logic that we execute to set it up the fixture setup.

设置夹具的最常见方式是使用前门夹具设置 - 即,调用 SUT 上的适当方法将其置于起始状态。这可能需要构造其他对象并将它们作为方法调用的参数传递给 SUT。当 SUT 的状态存储在其他对象或组件中时,我们可以执行后门设置(请参阅第327页的后门操作) - 即,我们可以将必要的记录直接插入到 SUT 行为所依赖的其他组件中。我们最常在数据库中使用后门设置,或者在需要使用模拟对象(第544页)或测试替身(第522页)时使用后门设置。这些可能性分别在第13 章“使用数据库进行测试”第 11 章使用测试替身”中介绍。

The most common way to set up the fixture is using front door fixture setup—that is, to call the appropriate methods on the SUT to put it into the starting state. This may require constructing other objects and passing them to the SUT as arguments of method calls. When the state of the SUT is stored in other objects or components, we can do Back Door Setup (see Back Door Manipulation on page 327)—that is, we can insert the necessary records directly into the other component on which the behavior of the SUT depends. We use Back Door Setup most often with databases or when we need to use a Mock Object (page 544) or Test Double (page 522). These possibilities are covered in Chapter 13, Testing with Databases, and Chapter 11, Using Test Doubles, respectively.

值得注意的是,“fixture”一词在不同类型的测试自动化中有不同的含义。Microsoft 语言的 xUnit 变体将Testcase Class (第 373页)称为测试fixture。大多数其他 xUnit 变体会区分Testcase Class和它设置的测试fixture(或测试上下文)。在 Fit [FitB]中,“fixture”一词用于表示我们用来定义高级语言(参见第 41 页)的数据驱动测试第 288页)解释器的定制部分。本书在提到“测试fixture”时,如未进一步限定该术语,则是指我们在执行 SUT 之前设置的东西。要引用承载测试方法(第348页)的类(无论是 Java 还是 C#、Ruby 还是 VB),本书均使用Testcase Class

It is worth noting that the term "fixture" is used to mean different things in different kinds of test automation. The xUnit variants for the Microsoft languages call the Testcase Class (page 373) the test fixture. Most other variants of xUnit distinguish between the Testcase Class and the test fixture (or test context) it sets up. In Fit [FitB], the term "fixture" is used to mean the custom-built parts of the Data-Driven Test (page 288) interpreter that we use to define our Higher-Level Language (see page 41). Whenever this book says "test fixture" without further qualifying this term, it refers to the stuff we set up before exercising the SUT. To refer to the class that hosts the Test Methods (page 348), whether it be in Java or C#, Ruby or VB, this book uses Testcase Class.

什么是新鲜装置?

What Is a Fresh Fixture?

在“新鲜夹具”策略中,我们为运行的每项测试设置一个全新的夹具(图 8.2)。也就是说,每个测试用例对象第 382页)在执行 SUT 之前都会构建自己的夹具,并且每次重新运行时都会这样做。这就是夹具“新鲜”的原因。因此,我们完全避免了与交互测试相关的问题(请参阅第228页的“不稳定测试”)。

In a Fresh Fixture strategy, we set up a brand-new fixture for every test we run (Figure 8.2). That is, each Testcase Object (page 382) builds its own fixture before exercising the SUT and does so every time it is rerun. That is what makes the fixture "fresh." As a result, we completely avoid the problems associated with Interacting Tests (see Erratic Test on page 228).

图 8.2。 一对 Fresh Fixture,每个都有其创建者。Fresh Fixture 专为单个测试而构建,使用一次,然后退出。

Figure 8.2. A pair of Fresh Fixtures, each with its creator. A Fresh Fixture is built specifically for a single test, used once, and then retired.

图像

什么是瞬态新鲜装置?

What Is a Transient Fresh Fixture?

当我们的 Fixture 是仅由局部变量或实例变量引用的内存 Fixture 时,1 Fixture 会在每次测试后通过垃圾收集拆卸第 500页)“消失”。当 Fixture 是持久性时,情况并非如此。因此,我们需要决定如何实施“新鲜 Fixture”策略。具体来说,我们有两种不同的方法可以保持它们“新鲜”。显而易见的选择是在每次测试后拆卸 Fixture。不太明显的选择是保留旧 Fixture,然后以不与旧 Fixture 冲突的方式构建新 Fixture。

When our fixture is an in-memory fixture referenced only by local variables or instance variables,1 the fixture just "disappears" after every test courtesy of Garbage-Collected Teardown (page 500). When fixtures are persistent, this is not the case. Thus we have some decisions to make about how we implement the Fresh Fixture strategy. In particular, we have two different ways to keep them "fresh." The obvious option is tear down the fixture after each test. The less obvious option is to leave the old fixture around and then build a new fixture in such a way that it does not collide with the old fixture.

我们构建的大多数Fresh Fixture都是临时的,所以我们将首先介绍这种情况。然后,我们将在第 9 章中回顾如何管理持久 Fresh Fixture

Most Fresh Fixtures we build are transient, so we will cover that case first. We will then come back to managing Persistent Fresh Fixtures in Chapter 9.

打造新鲜装置

Building Fresh Fixtures

无论我们构建的是瞬态新鲜夹具还是持久新鲜夹具,我们在构建它时可以选择的方式几乎相同。夹具设置逻辑包括实例化 SUT 所需的代码、2将 SUT 置于适当的起始状态的代码,以及创建和初始化 SUT 所依赖的任何内容或将作为参数传递给它的任何内容的状态的代码。设置新鲜夹具最明显的方法是通过内联设置第 408页),其中所有夹具设置逻辑都包含在测试方法中。这种类型的夹具也可以通过使用委托设置第 411页)来构建,其中涉及调用测试实用程序方法第 599页)。最后,我们可以使用隐式设置(第424页),其中测试自动化框架(第298页)调用我们在测试用例类setUp上提供的特殊方法。我们还可以结合使用这三种方法。让我们分别看看每一种可能性。

Whether we are building a Transient Fresh Fixture or a Persistent Fresh Fixture, the choices we have for how to construct it are pretty much the same. The fixture setup logic includes the code needed to instantiate the SUT,2 the code to put the SUT into the appropriate starting state, and the code to create and initialize the state of anything the SUT depends on or that will be passed to it as an argument. The most obvious way to set up a Fresh Fixture is through In-line Setup (page 408), in which all fixture setup logic is contained within the Test Method. This type of fixture can also be constructed by using Delegated Setup (page 411), which involves calling Test Utility Methods (page 599). Finally, we can use Implicit Setup (page 424), in which the Test Automation Framework (page 298) calls a special setUp method we provide on our Testcase Class. We can also use a combination of these three approaches. Let's look at each possibility individually.

在线夹具设置

In-line Fixture Setup

在内联设置中,测试处理测试方法主体内的所有装置设置。我们构造对象、调用其方法、构造 SUT 并调用其方法使其进入特定状态。我们在测试方法中执行所有这些任务。将内联设置视为装置创建的 DIY 方法。

In In-line Setup, the test handles all of the fixture setup within the body of the Test Method. We construct objects, call methods on them, construct the SUT, and call methods on it to put into a specific state. We perform all of these tasks from within our Test Method. Think of In-line Setup as the do-it-yourself approach to fixture creation.

public void testStatus_initial() {

      // 在线设置

      Airport attendance.next.println(“Calgary”);

      airport appointmentAirport = new Airport(“Toronto ”);

      flight flight = new Flight ( flightNumber,

                                                   attendance,

                                                   destinationAirport);

      // 练习 SUT 并验证结果

     assertEquals(FlightState.PROPOSED, flight.getStatus());

      // 拆卸:

         // 垃圾收集

}

public  void  testStatus_initial()  {

      //  In-line  setup

      Airport  departureAirport  =  new  Airport("Calgary",  "YYC");

      Airport  destinationAirport  =  new  Airport("Toronto",  "YYZ");

      Flight  flight  =  new  Flight  (  flightNumber,

                                                   departureAirport,

                                                   destinationAirport);

      //  Exercise  SUT  and  verify  outcome

     assertEquals(FlightState.PROPOSED,  flight.getStatus());

      //  tearDown:

         //       Garbage-collected

}

 

内联设置的主要缺点是它往往会导致测试代码重复(第 213页),因为每种测试方法都需要构建 SUT。许多测试方法还需要执行类似的装置设置。这种测试代码重复反过来又会导致由脆弱测试(第 239页) 引起的高测试维护成本(第265页)。如果创建装置的工作很复杂,还会导致模糊测试(第186页)。一个相关的问题是,内联设置往往鼓励每种测试方法中的硬编码测试数据(参见模糊测试) ,因为创建具有意图揭示名称[SBPP] 的局部变量似乎工作量太大,而收益却不大。

The main drawback of In-line Setup is that it tends to lead to Test Code Duplication (page 213) because each Test Method needs to construct the SUT. Many of the Test Methods also need to perform similar fixture setup. This Test Code Duplication leads, in turn, to High Test Maintenance Cost (page 265) caused by Fragile Tests (page 239). If the work to create the fixture is complex, it can also lead to Obscure Tests (page 186). A related problem is that In-line Setup tends to encourage Hard-Coded Test Data (see Obscure Test) within each Test Method because creating a local variable with an Intent-Revealing Name [SBPP] may seem like too much work for the benefit yielded.

我们可以通过将设置夹具的代码移出测试方法来防止这些测试异味。我们将其移动到的位置决定了我们使用了哪种备选夹具设置策略。

We can prevent these test smells by moving the code that sets up the fixture out of the Test Method. The location where we move it determines which of the alternative fixture setup strategies we have used.

委托装置设置

Delegated Fixture Setup

减少测试代码重复和随之而来的模糊测试的一种快速简便的方法是重构我们的测试方法以使用委托设置。我们可以使用提取方法 [Fowler] 重构将几个测试方法中使用的一系列语句移动到一个测试实用程序方法中,然后从这些测试方法中调用该方法。这是一个非常简单和安全的重构,特别是当我们让 IDE 为我们完成所有繁重工作时。当提取的方法包含创建测试所依赖的对象的逻辑时,我们称之为创建方法(第415页)。具有意图揭示名称的创建方法3使测试的先决条件对读者来说显而易见,同时避免了不必要的测试代码重复。它们允许测试读取器和测试自动化程序专注于正在创建的内容,而不会被创建方式分散注意力。创建方法充当测试夹具构建的可重用构建块。

A quick and easy way to reduce Test Code Duplication and the resulting Obscure Tests is to refactor our Test Methods to use Delegated Setup. We can use an Extract Method [Fowler] refactoring to move a sequence of statements used in several Test Methods into a Test Utility Method that we then call from those Test Methods. This is a very simple and safe refactoring, especially when we let the IDE do all the heavy lifting for us. When the extracted method contains logic to create an object on which our test depends, we call it a Creation Method (page 415). Creation Methods3 with Intent-Revealing Names make the test's pre-conditions readily apparent to the reader while avoiding unnecessary Test Code Duplication. They allow both the test reader and the test automater to focus on what is being created without being distracted by how it is created. The Creation Methods act as reusable building blocks for test fixture construction.

public void testGetStatus_inital() {

       // 设置

      Flight flight = createAnonymousFlight();

      // 练习 SUT 并验证结果

     assertEquals(FlightState.PROPOSED, flight.getStatus());

      // 拆卸

      // 垃圾收集

}

public  void  testGetStatus_inital()  {

       //  Setup

      Flight  flight  =  createAnonymousFlight();

      //  Exercise  SUT  and  verify  outcome

     assertEquals(FlightState.PROPOSED,  flight.getStatus());

      //  Teardown

      //         Garbage-collected

}

 

这些创建方法的一个目标是使每个测试无需知道所需对象是如何创建的细节。这种简化对于防止由于构造函数方法签名或语义的更改而导致的脆弱测试大有帮助。如果测试不关心它所创建对象的具体身份,则我们可以使用匿名创建方法(请参阅创建方法)。这些方法生成所创建对象所需的任何唯一键。通过使用独特的生成值(请参阅第723页的生成值我们可以保证其他需要类似对象的测试实例不会意外使用与此测试相同的对象。即使我们碰巧使用支持共享装置的持久对象存储库,这种保护措施也可以防止多种形式的行为异味“不稳定测试”,包括不可重复的测试交互测试测试运行战争。

One goal of these Creation Methods is to eliminate the need for every test to know the details of how the objects it requires are created. This streamlining goes a long way toward preventing Fragile Tests caused by changes to constructor method signatures or semantics. When a test does not care about the specific identity of the objects it is creating, we can use Anonymous Creation Methods (see Creation Method). These methods generate any unique keys required by the object being created. By using a Distinct Generated Value (see Generated Value on page 723), we can guarantee that no other test instance that requires a similar object will accidentally use the same object as this test. This safeguard prevents many forms of the behavior smell Erratic Test, including Unrepeatable Tests, Interacting Tests, and Test Run Wars, even if we happen to be using a persistent object repository that supports Shared Fixtures.

当测试确实关心正在创建的对象的属性时,我们使用参数化匿名创建方法(请参阅创建方法。此方法传递测试关心的任何属性(即对测试结果很重要的属性),而其他所有属性则由创建方法的实现默认。我的座右铭是:

When a test does care about the attributes of the object being created, we use a Parameterized Anonymous Creation Method (see Creation Method). This method is passed any attributes that the test cares about (i.e., attributes that are important to the test outcome), leaving all other attributes to be defaulted by the implementation of the Creation Method. My motto is this:

当某些东西在测试方法中被看到并不重要时,让它在测试方法中不被看到就很重要!

When it is not important for something to be seen in the test method, it is important that it not be seen in the test method!

 

委托设置在为需要验证对象参数属性的 SUT 方法编写输入验证测试时经常使用。在这种情况下,我们需要为应该检测的每个无效属性编写单独的测试。使用内联设置构建所有这些稍微无效的对象需要大量工作。我们可以通过使用一个错误属性(请参阅718页的派生值)模式大大减少工作量和测试代码重复的数量。也就是说,我们首先调用创建方法来创建一个有效对象,然后用 SUT 应该拒绝的无效值替换一个属性。同样,我们可以使用命名状态到达方法(请参阅创建方法)创建处于正确状态的对象

Delegated Setup is often used when we write input validation tests for SUT methods that are expected to validate the attributes of an object argument. In such a case, we need to write a separate test for each invalid attribute that should be detected. Building all of these slightly invalid objects would be a lot of work using In-line Setup. We can reduce the effort and the amount of Test Code Duplication dramatically by using the pattern One Bad Attribute (see Derived Value on page 718). That is, we first call a Creation Method to create a valid object, and then we replace one attribute with an invalid value that should be rejected by the SUT. Similarly, we might create an object in the correct state by using a Named State Reaching Method (see Creation Method).

有些人喜欢使用重用测试进行装置设置(请参阅创建方法),而不是使用链式测试第 454页)。也就是说,他们直接在测试的设置部分调用其他测试。只要测试阅读器可以轻松识别其他测试为当前测试设置的内容,这种方法就不算不合理。不幸的是,很少有测试的命名方式能够传达这种意图。因此,如果我们重视测试作为文档,我们将要考虑用具有意图揭示名称的创建方法包装被调用的测试,以便测试阅读器可以了解装置的样子。

Some people prefer to Reuse Tests for Fixture Setup (see Creation Method) as an alternative to using Chained Tests (page 454). That is, they call other tests directly within the setup portion of their test. This approach is not an unreasonable one as long as the test reader can readily identify what the other test is setting up for the current test. Unfortunately, very few tests are named in such a way as to convey this intention. For this reason, if we value Tests as Documentation, we will want to consider wrapping the called test with a Creation Method that has an Intent-Revealing Name so that test reader can get a sense of what the fixture looks like.

创建方法可以作为Testcase 类的私有方法保留,也可以拉到Testcase 超类(第 638页),或者移到Test Helper (第643页)。“所有创建方法的母体”是Object Mother(请参阅Test Helper)。此策略级模式描述了一系列方法,这些方法以在一个或多个Test Helper上使用创建方法为中心,并且可能还包括自动拆卸(第503页)。

The Creation Methods can be kept as private methods on the Testcase Class, pulled up to a Testcase Superclass (page 638), or moved to a Test Helper (page 643). The "mother of all creation methods" is Object Mother (see Test Helper). This strategy-level pattern describes a family of approaches that center on the use of Creation Methods on one or more Test Helpers and may include Automated Teardown (page 503) as well.

隐式夹具设置

Implicit Fixture Setup

xUnit 家族的大多数成员都提供了一个方便的钩子,用于调用需要在每个测试方法之前运行的代码。一些成员调用具有特定名称的方法(例如setUp)。其他成员调用具有特定注释(例如 JUnit 中的“@before”)或方法属性(例如SetupNUnit 中的“[ ]”)的方法。为了避免每次需要引用此机制时都重复这些替代方法,本书只是将其称为setUp方法,而不管我们如何向测试自动化框架指出这一事实。setUp方法是可选的,或者框架提供了一个空的默认实现,因此我们不必在每个测试用例类中都提供一个。

Most members of the xUnit family provide a convenient hook for calling code that needs to be run before every Test Method. Some members call a method with a specific name (e.g., setUp). Others call a method that has a specific annotation (e.g., "@before" in JUnit) or method attribute (e.g., "[Setup]" in NUnit). To avoid repeating these alternative ways every time we need to refer to this mechanism, this book simply calls it the setUp method regardless of how we indicate this fact to the Test Automation Framework. The setUp method is optional or an empty default implementation is provided by the framework, so we do not have to provide one in each Testcase Class.

隐式设置中,我们利用这个框架“钩子”,将所有夹具创建逻辑放入setUp方法中。由于测试用例类上的每个测试方法都共享此夹具设置逻辑,因此所有测试方法都需要能够使用它创建的夹具。这种策略当然解决了测试代码重复问题,但它确实有几个后果。以下测试实际上验证了什么?

In Implicit Setup, we take advantage of this framework "hook" by putting all of the fixture creation logic into the setUp method. Because every Test Method on the Testcase Class shares this fixture setup logic, all Test Methods need to be able to use the fixture it creates. This tactic certainly addresses the Test Code Duplication problem but it does have several consequences. What does the following test actually verify?

机场出发机场;

机场目的地机场;

航班航班;

public void testGetStatus_inital() {

      //隐式设置

      // 练习 SUT 并验证结果

     assertEquals(FlightState.PROPOSED, flight.getStatus());

}

Airport  departureAirport;

Airport  destinationAirport;

Flight  flight;

public  void  testGetStatus_inital()  {

      //  Implicit  setup

      //  Exercise  SUT  and  verify  outcome

     assertEquals(FlightState.PROPOSED,  flight.getStatus());

}

 

第一个后果是,这种方法会使测试更难理解,因为我们无法看到测试的先决条件(测试装置)与测试方法中的预期结果如何关联;我们必须在setUp方法中才能看到这种关系。

The first consequence is that this approach can make the tests more difficult to understand because we cannot see how the pre-conditions of the test (the test fixture) correlate with the expected outcome within the Test Method; we have to look in the setUp method to see this relationship.

public void setUp() 抛出异常{

      super.setUp();

      departureAirport = new Airport("卡尔加里", "YYC");

      destinationAirport = new Airport("多伦多", "YYZ");

      BigDecimal flightNumber = new BigDecimal("999");

      flight = new Flight( flightNumber , departureAirport,

                                        destinationAirport);

}

public  void  setUp()  throws  Exception{

      super.setUp();

      departureAirport  =  new  Airport("Calgary",  "YYC");

      destinationAirport  =  new  Airport("Toronto",  "YYZ");

      BigDecimal  flightNumber  =  new  BigDecimal("999");

      flight  =  new  Flight(  flightNumber  ,  departureAirport,

                                        destinationAirport);

}

 

我们可以通过根据方法中创建的测试装置命名我们的测试用例类setUp来缓解此问题。当然,这只有在所有测试方法确实需要相同的装置时才有意义——这是每个装置一个测试用例类的示例(第 631页)。如前所述,xUnit 家族的几个成员(VbUnit 和 NUnit,仅举两个例子)使用术语“测试装置”来描述本书所称的测试用例类。这种命名法可能基于我们正在使用每个装置一个测试用例类策略的假设。

We can mitigate this problem by naming our Testcase Class based on the test fixture created in the setUp method. Of course, this makes sense only if all of the Test Methods really need the same fixture—it is an example of Testcase Class per Fixture (page 631). As mentioned earlier, several members of the xUnit family (VbUnit and NUnit, to name two) use the term "test fixture" to describe what this book calls the Testcase Class. This nomenclature is probably based on the assumption that we are using a Testcase Class per Fixture strategy.

使用隐式设置的另一个后果是,我们不能使用局部变量来保存对我们装置中对象的引用。相反,我们被迫使用实例变量来引用在setUp方法中构造的任何对象,这些对象在执行 SUT、验证预期结果或拆除装置时都需要。这些实例变量充当测试各部分之间的全局变量。但是,只要我们坚持使用实例变量而不是类变量,测试装置就会为Testcase Class中的每个测试用例重新构造。xUnit 的大多数成员在为每个测试方法创建的装置之间提供隔离,但至少有一个(NUnit)没有;有关更多信息,请参阅侧栏“总是有异常”(第 384页)。无论如何,我们绝对应该给变量赋予意图揭示名称,这样我们就不需要一直引用方法setUp来了解它们包含的内容。

Another consequence of using Implicit Setup is that we cannot use local variables to hold references to the objects in our fixture. Instead, we are forced to use instance variables to refer to any objects that are constructed in the setUp method and that are needed either when exercising the SUT, when verifying the expected outcome, or when tearing down the fixture. These instance variables act as global variables between the parts of the test. As long as we stick to instance variables rather than class variables, however, the test fixture will be newly constructed for each test case in the Testcase Class. Most members of xUnit provide isolation between the fixture created for each Test Method but at least one (NUnit) does not; see the sidebar "There's Always an Exception" (page 384) for more information. In any event, we should definitely give the variables Intent-Revealing Names so that we do not need to keep referring back to the setUp method to understand what they hold.

滥用 SetUp 方法

当你有一把新锤子时,一切看起来都像钉子!

When you have a new hammer, everything looks like a nail!

与任何系统的任何功能一样,该setUp方法也可能被滥用。我们不应该仅仅因为它被提供就觉得有义务使用它。它是我们应用程序可用的几种代码重用机制之一。当面向对象语言首次推出时,程序员迷恋继承并试图在所有可能的重用场景中应用它。随着时间的推移,我们了解了何时继承是合适的,何时应该诉诸其他机制(如委托)。该setUp方法就是 xUnit 的继承。

Like any feature of any system, the setUp method can be abused. We should not feel obligated to use it just because it is provided. It is one of several code reuse mechanisms that are available for our application. When object-oriented languages were first introduced, programmers were enamored with inheritance and tried to apply it in all possible reuse scenarios. Over time, we learned when inheritance was appropriate and when we should resort to other mechanisms such as delegation. The setUp method is xUnit's inheritance.

当该setUp方法用于构建具有多个不同部分的通用夹具(参见模糊测试)时,最容易被误用,每个部分都专用于不同的测试方法。如果我们构建的是持久性新鲜夹具,这可能会导致测试缓慢第 253页) 更重要的是,它可以通过隐藏夹具与执行 SUT 的预期结果之间的因果关系来导致模糊测试。

The setUp method is most prone to misuse when it is applied to build a General Fixture (see Obscure Test) with multiple distinct parts, each of which is dedicated to a different Test Method. This can lead to Slow Tests (page 253) if we are building a Persistent Fresh Fixture. More importantly, it can lead to Obscure Tests by hiding the cause–effect relationship between the fixture and the expected outcome of exercising the SUT.

如果我们不采用基于相同夹具将测试方法分组到测试用例类的做法,但确实使用了该setUp方法,则应在方法中仅构建夹具的最小公分母部分setUp。也就是说,只应将不会在任何测试中引起问题的设置逻辑放在方法中setUp。如果我们使用该方法构建通用夹具而不是最小夹具第 302页),即使夹具设置代码不会对任何测试方法造成问题,也仍然可能导致其他问题。通用夹具导致测试缓慢的常见原因,因为每个测试花费的时间都比构建测试夹具所需的时间多得多。它还倾向于产生模糊测试,因为测试读取器无法轻松看到特定测试方法依赖于夹具的哪个部分。通用夹具经常演变为脆弱夹具(参见脆弱测试),因为其各个元素与使用它们的测试之间的关系随着时间的推移而被遗忘。对夹具进行更改以支持新添加的测试可能会导致现有测试失败。setUp

If we do not adopt the practice of grouping the Test Methods into Testcase Classes based on identical fixtures but we do use the setUp method, we should build only the lowest common denominator part of the fixture in the setUp method. That is, only the setup logic that will not cause problems in any of the tests should be placed in the setUp method. Even the fixture setup code that does not cause problems for any of the Test Methods can still cause other problems if we use the setUp method to build a General Fixture instead of a Minimal Fixture (page 302). A General Fixture is a common cause of Slow Tests because each test spends much more time than necessary building the test fixture. It also tends to produce Obscure Tests because the test reader cannot easily see which part of the fixture a particular Test Method depends on. A General Fixture often evolves into a Fragile Fixture (see Fragile Test) as the relationship between its various elements and the tests that use them is forgotten over time. Changes made to the fixture to support a newly added test may then cause existing tests to fail.

请注意,如果我们使用类变量来保存对象,我们可能已经越界进入了持久新装置的世界。相反,使用惰性设置第 435页)来填充变量,会将我们带入共享装置的世界,因为测试套件中的后续测试可能会重用在早期测试中创建的对象,因此可能会依赖于其他测试(应该)对其所做的更改。

Note that if we use a class variable to hold the object, we may have crossed the line into the world of Persistent Fresh Fixtures. Use of Lazy Setup (page 435) to populate the variable, by contrast, carries us into the world of Shared Fixtures because later tests within the test suite may reuse the object(s) created in earlier tests and thus may become dependent on the changes the other test (should have) made to it.

混合夹具设置

Hybrid Fixture Setup

本章介绍了三种夹具构造样式,它们是彼此的严格替代方案。在实践中,将它们结合起来很有价值。测试自动化程序通常会从测试方法中调用一些创建方法,然后在内联基础上进行一些额外的设置。如果方法调用创建方法来构造夹具,那么方法的可读性也可以得到改善。另一个好处是,与内联夹具构造逻辑或方法相比,创建方法可以更容易地进行单元测试。这些方法也可以位于测试用例类层次结构之外的类中,例如测试助手setUpsetUp

This chapter has presented the three styles of fixture construction as strict alternatives to one another. In practice, there is value in combining them. Test automaters often call some Creation Methods from within the Test Method but then do some additional setup on an in-line basis. The readability of the setUp method can also be improved if it calls Creation Methods to construct the fixture. An additional benefit is that the Creation Methods can be unit-tested much more easily than either in-line fixture construction logic or the setUp method. These methods can also be located on a class outside the Testcase Class hierarchy such as a Test Helper.

拆除临时的新鲜装置

Tearing Down Transient Fresh Fixtures

瞬态新鲜装置 (Transient Fresh Fixtures)的一个真正优点是装置拆卸几乎不需要任何工作。xUnit 家族的大多数成员都是用支持垃圾收集的语言实现的。只要我们对装置的引用保存在超出范围的变量中,我们就可以依靠垃圾收集拆卸为我们完成所有工作。请参阅第384页的侧栏“总是有异常”以了解为什么 NUnit 中的情况并非如此。

One really nice thing about Transient Fresh Fixtures is that fixture teardown requires very little effort. Most members of the xUnit family are implemented in languages that support garbage collection. As long as our references to the fixture are held in variables that go out of scope, we can count on Garbage-Collected Teardown to do all the work for us. See the sidebar "There's Always an Exception" on page 384 for a description of why the same is not true in NUnit.

如果我们使用的是 xUnit 家族中不支持垃圾收集的少数成员之一,我们可能必须将所有Fresh Fixtures视为持久的。

If we are using one of the few members of the xUnit family that does not support garbage collection, we may have to treat all Fresh Fixtures as persistent.

下一步是什么?

What's Next?

本章介绍了设置和拆除内存中Fresh Fixture的技术。只要有一点计划和运气,它们就足以完成大多数测试。当 Fixture 由 SUT 或测试本身持久化时,管理Fresh Fixture会更加复杂。第 9 章持久 Fixture 管理”介绍了管理持久 Fixture 所需的其他技术,包括持久 Fresh Fixture共享 Fixture

This chapter introduced techniques for setting up and tearing down an in-memory Fresh Fixture. With some planning and a bit of luck, they are all you should need for the majority of your tests. Managing Fresh Fixtures is more complicated when the fixture is persisted either by the SUT or by the test itself. Chapter 9, Persistent Fixture Management, introduces additional techniques needed for managing persistent fixtures, including Persistent Fresh Fixtures and Shared Fixtures.

第 9 章

持久性 Fixture 管理

Chapter 9

Persistent Fixture Management

 

关于本章

About This Chapter

第 8 章“瞬态夹具管理”中,我们了解了如何构建内存中的新鲜夹具(第311页)。我们在该章中指出,当夹具由 SUT 或测试本身持久化时,管理新鲜夹具会更加复杂。本章介绍了管理持久夹具所需的其他模式,包括持久新鲜夹具(参见新鲜夹具)和共享夹具(第317页)。

In Chapter 8, Transient Fixture Management, we saw how we can go about building in-memory Fresh Fixtures (page 311). We noted in that chapter that managing Fresh Fixtures is more complicated when the fixture is persisted either by the SUT or by the test itself. This chapter introduces the additional patterns required to manage persistent fixtures, including both Persistent Fresh Fixtures (see Fresh Fixture) and Shared Fixtures (page 317).

管理持久的新鲜装置

Managing Persistent Fresh Fixtures

持久新鲜夹具这个术语听起来可能像一个矛盾的说法,但实际上它并没有乍看起来那么矛盾。新鲜夹具策略意味着每个测试方法(第348页) 的每次运行都使用一个新创建的夹具。这个名字说明了它的目的:我们不会重用该夹具!它并不一定暗示该夹具是瞬态的,只要暗示它不会被重用即可 (图 9.1 )。持久新鲜夹具带来了几个瞬态新鲜夹具不会遇到的挑战在本章中,我们重点关注由剩余的持久新鲜夹具导致的不可重复测试(请参阅第228页的不稳定测试)和由共享夹具(317页) 导致的慢速测试(第 253页)所带来的挑战。

The term Persistent Fresh Fixture might sound like an oxymoron but it is actually not as large a contradiction as it might first seem. The Fresh Fixture strategy means that each run of each Test Method (page 348) uses a newly created fixture. The name speaks to its intent: We do not reuse the fixture! It does not need to imply that the fixture is transient—only that it is not reused (Figure 9.1). Persistent Fresh Fixtures present several challenges not encountered with Transient Fresh Fixtures. In this chapter, we focus on the challenge posed by Unrepeatable Tests (see Erratic Test on page 228) caused by leftover Persistent Fresh Fixtures and Slow Tests (page 253) caused by Shared Fixtures (page 317).

图 9.1。 新鲜装置可以是临时的,也可以是持久的。即使测试装置是自然持久的,我们也可以应用新鲜装置策略,但我们必须有一种在每次测试后将其拆除的方法。

Figure 9.1. A Fresh Fixture can be either transient or persistent. We can apply a Fresh Fixture strategy even if the test fixture is naturally persistent but we must have a way to tear it down after each test.

图像

什么使得 Fixtures 持久化?

What Makes Fixtures Persistent?

固定装置(无论是新的还是其他的)可以由于以下两个原因之一而变得持久。第一个原因是 SUT 是一个有状态的对象,并且“记住”了它过去的使用方式。这种情况最常发生在 SUT 包含数据库时,但它也可能仅仅因为 SUT 使用类变量来保存其部分数据而发生。第二个原因是Testcase 类第 373页)保存了对其他瞬态新鲜固定装置的引用,使其在测试方法调用中存活下来。

A fixture, fresh or otherwise, can become persistent for one of two reasons. The first reason is that the SUT is a stateful object and "remembers" how it was used in the past. This scenario most often occurs when the SUT includes a database, but it can occur simply because the SUT uses class variables to hold some of its data. The second reason is that the Testcase Class (page 373) holds a reference to an otherwise Transient Fresh Fixture in a way that makes it survive across Test Method invocations.

xUnit 系列的一些成员提供了一种在每次测试运行开始时重新加载所有类的机制。此行为可能显示为一个选项(标记为“重新加载类”的复选框),也可能是自动的。当从类变量引用 Fixture 时,此功能可防止该 Fixture 变为持久性;如果 SUT 或测试将 Fixture 放入文件系统或数据库中,则它不会阻止Fresh Fixture变为持久性。

Some members of the xUnit family provide a mechanism to reload all classes at the beginning of each test run. This behavior may appear as an option—a check box labeled "Reload Classes"—or it may be automatic. Such a feature keeps the fixture from becoming persistent when it is referenced from a class variable; it does not prevent the Fresh Fixture from becoming persistent if either the SUT or the test puts the fixture into the file system or a database.

持续使用新鲜装置引起的问题

Issues Caused by Persistent Fresh Fixtures

当 Fixture 是持久性的时,我们可能会发现,相同测试方法的后续运行会尝试重新创建已存在的 Fixture。此行为可能会导致现有资源与新创建的资源之间发生冲突。虽然违反数据库中的唯一键约束是此问题最常见的示例,但冲突可能很简单,例如尝试创建与已存在文件同名的文件。避免这些不可重复测试的一种方法是在每个测试结束时拆除 Fixture;另一种方法是对可能导致冲突的任何标识符使用不同的生成值(请参阅第723页的生成值)。

When fixtures are persistent, we may find that subsequent runs of the same Test Method try to recreate a fixture that already exists. This behavior may cause conflicts between the preexisting and newly created resources. Although violating unique key constraints in the database is the most common example of this problem, the conflict could be as simple as trying to create a file with the same name as one that already exists. One way to avoid these Unrepeatable Tests is to tear down the fixture at the end of each test; another is to use Distinct Generated Values (see Generated Value on page 723) for any identifiers that might cause conflicts.

拆除持久的新鲜装置

Tearing Down Persistent Fresh Fixtures

与夹具设置代码不同,夹具设置代码应该可以帮助我们理解测试的先决条件,而夹具拆卸代码则纯粹是管理方面的问题。它不能帮助我们理解 SUT 的行为,但可能会掩盖测试的意图,或者至少使其更难理解。因此,最好的拆卸代码是不存在的那种。我们应尽可能避免编写拆卸代码,这就是为什么垃圾收集拆卸(第500页)如此可取的原因。不幸的是,如果我们的Fresh Fixture是持久的,我们就无法利用垃圾收集拆卸

Unlike fixture setup code, which should help us understand the pre-conditions of the test, fixture teardown code is purely a matter of good housekeeping. It does not help us understand the behavior of the SUT but it has the potential to obscure the intent of the test or at least make it more difficult to understand. Therefore, the best kind of teardown code is the nonexistent kind. We should avoid writing teardown code whenever we can, which is why Garbage-Collected Teardown (page 500) is so preferable. Unfortunately, we cannot take advantage of Garbage-Collected Teardown if our Fresh Fixture is persistent.

手工编码的拆解

确保装置在使用完毕后被销毁的一种方法是将测试专用的拆卸代码包含在测试方法中。这种拆卸机制看似简单,但实际上比乍一看的要复杂得多。请考虑以下示例:

One way to ensure that the fixture is destroyed after we are done with it is to include test-specific teardown code within our Test Methods. This teardown mechanism might seem simple, but it is actually more complex than immediately meets the eye. Consider the following example:

            public void testGetFlightsByOriginAirport_NoFlights()

                           throws Exception {

                  // Fixture Setup

                  BigDecimal outboundAirport = createTestAirport("1OF");

                  // 练习系统

                  列表 flightsAtDestination1 =

                             Facade.getFlightsByOriginAirport(outboundAirport);

                  // 验证结果

                  assertEquals(0,flightsAtDestination1.size());

                  Facade.removeAirport(outboundAirport);

              }

            public  void  testGetFlightsByOriginAirport_NoFlights()

                           throws  Exception  {

                  //  Fixture  Setup

                  BigDecimal  outboundAirport  =  createTestAirport("1OF");

                  //  Exercise  System

                  List  flightsAtDestination1  =

                             facade.getFlightsByOriginAirport(outboundAirport);

                  //  Verify  Outcome

                  assertEquals(0,flightsAtDestination1.size());

                  facade.removeAirport(outboundAirport);

              }

 

这种简单的内联拆卸(请参阅第 509页的内联拆卸)将在测试通过时拆卸夹具 - 但如果测试失败或以错误结束,它不会拆卸夹具。这是因为对断言方法第 362页)的调用会引发异常;因此,我们可能永远不会到达拆卸代码。为了确保内联拆卸代码始终执行,我们必须用控制结构包围测试方法中可能引发异常的所有内容。以下是经过适当修改的相同测试:try/catch

This Naive In-line Teardown (see In-line Teardown on page 509) will tear down the fixture when the test passes—but it won't tear down the fixture if the test fails or ends with an error. This is because the calls to the Assertion Methods (page 362) throw an exception; therefore, we may never make it to the teardown code. To ensure that the In-line Teardown code always executes, we must surround everything in the Test Method that might raise an exception with a try/catch control structure. Here's the same test suitably modified:

      public void testGetFlightsByOriginAirport_NoFlights_td()

                     throws Exception {

            // Fixture Setup

            BigDecimal outboundAirport = createTestAirport("1OF");

            try {

                  // Exercise System

                  List flightsAtDestination1 =

                            Facade.getFlightsByOriginAirport(outboundAirport);

                            // 验证结果

                            assertEquals(0,flightsAtDestination1.size());

                      } finally {

                              facade.removeAirport(outboundAirport);

                      }

              }

      public  void  testGetFlightsByOriginAirport_NoFlights_td()

                     throws  Exception  {

            //  Fixture  Setup

            BigDecimal  outboundAirport  =  createTestAirport("1OF");

            try  {

                  //  Exercise  System

                  List  flightsAtDestination1  =

                            facade.getFlightsByOriginAirport(outboundAirport);

                            //  Verify  Outcome

                            assertEquals(0,flightsAtDestination1.size());

                      }  finally  {

                              facade.removeAirport(outboundAirport);

                      }

              }

 

遗憾的是,确保拆卸代码始终运行的机制给测试方法带来了相当大的复杂性。当必须拆卸多个资源时,事情会变得更加复杂:即使清理一种资源的尝试失败,我们也希望确保其他资源仍然会被清理。我们可以通过使用提取方法 [Fowler] 重构将拆卸代码移到测试实用程序方法(第 599页)中来解决部分问题,我们从错误处理构造内部调用该方法。虽然这种委托拆卸(请参阅内联拆卸)隐藏了处理拆卸错误的复杂性,但我们仍然需要确保即使发生测试错误或测试失败时也会调用该方法。

Unfortunately, the mechanism to ensure that the teardown code always runs introduces a fair bit of complication into the Test Method. Matters become even more complicated when we must tear down several resources: Even if our attempt to clean up one resource fails, we want to ensure that the other resources are still cleaned up. We can address part of this problem by using Extract Method [Fowler] refactoring to move the teardown code into a Test Utility Method (page 599) that we call from inside the error-handling construct. Although this Delegated Teardown (see In-line Teardown) hides the complexity of dealing with teardown errors, we still need to ensure that the method gets called even when test errors or test failures occur.

xUnit 家族的大多数成员都通过支持隐式拆卸第 516页)解决了这个问题。测试自动化框架(第 298页)tearDown在每个测试方法之后调用一个特殊方法,无论测试通过与否。这种方法避免了将错误处理代码放在测试方法中,但对我们的测试提出了两个要求。首先,必须可以从方法访问 Fixture ,因此我们必须使用实例变量(首选)、类变量或全局变量来保存 Fixture。其次tearDown,我们必须确保该tearDown方法可以与每个测试方法一起正常工作,无论它设置了哪个 Fixture。1

Most members of the xUnit family solve this problem by supporting Implicit Teardown (page 516). The Test Automation Framework (page 298) calls a special tearDown method after each Test Method regardless of whether the test passed or failed. This approach avoids placing the error-handling code within the Test Method but imposes two requirements on our tests. First, the fixture must be accessible from the tearDown method, so we must use instance variables (preferred), class variables, or global variables to hold the fixture. Second, we must ensure that the tearDown method works properly with each of the Test Methods regardless of which fixture it sets up.1

匹配设置与拆卸代码组织

鉴于组织安装代码的三种方式——内联安装(第408页)、委托安装第 411页)和隐式安装(第424页)——以及组织拆卸代码的三种方式——内联拆卸、委托拆卸隐式拆卸——我们有九种不同的组合可供选择。选择正确的一种是一个简单的决定,因为拆卸代码是否对测试读者可见并不重要。我们只需选择最合适的安装代码组织方式,以及等效或更隐蔽的拆卸版本(表 9.1)。例如,即使与内联安装委托安装一起使用隐式拆卸也是合适的;将内联拆卸与内联安装以外的任何方式一起使用几乎从来都不是一个好主意,即使如此,也应该避免!

Given the three ways of organizing our setup code—In-line Setup (page 408), Delegated Setup (page 411), and Implicit Setup (page 424)—and the three ways of organizing our teardown code—In-line Teardown, Delegated Teardown, and Implicit Teardown—nine different combinations are available to us. Choosing the right one turns out to be an easy decision because it is not important for the teardown code to be visible to the test reader. We simply choose the most appropriate setup code organization and either the equivalent or more hidden version of teardown (Table 9.1). For example, it is appropriate to use Implicit Teardown even with In-line Setup or Delegated Setup; it is almost never a good idea to use In-line Teardown with anything other than In-line Setup, and even then it should probably be avoided!

表 9.1. 持久测试装置的各种装置设置和拆卸策略的兼容性

Table 9.1. The Compatibility of Various Fixture Setup and Teardown Strategies for Persistent Test Fixtures

图像

自动拆卸

手工编写拆卸代码有两个问题:编写测试需要额外的工作,拆卸代码很难正确,测试起来更困难。当拆卸出错时,可能会导致由资源泄漏引起的不稳定测试,因为失败的测试通常与未正确清理的测试不同。

Hand-coded teardown is associated with two problems: Extra work is required to write the tests, and the teardown code is hard to get right and even harder to test. When the teardown goes wrong, it may lead to Erratic Tests caused by Resource Leakage because the test that fails as a result is often different from the one that didn't clean up properly.

在支持垃圾收集的语言中,拆除瞬态新鲜装置应该是自动的。只要我们的装置仅由在测试用例对象(第 382页) 被销毁时超出范围的实例变量引用,垃圾收集就会清理它们。但是,如果我们使用类变量,或者我们的装置包含持久对象(如文件或数据库行),垃圾收集将不起作用。在这些情况下,我们需要执行自己的清理。

In languages that support garbage collection, tearing down a Transient Fresh Fixture should be pretty much automatic. As long as our fixtures are referenced only by instance variables that go out of scope when our Testcase Object (page 382) is destroyed, garbage collection will clean them up. Garbage collection won't work, however, if we use class variables or if our fixtures include persistent objects such as files or database rows. In those cases, we need to perform our own cleanup.

毫不奇怪,这种情况可能会激发懒惰但富有创造力的程序员想出一种方法来自动化拆卸逻辑。需要注意的重要一点是,拆卸代码不会帮助我们理解测试,因此最好其隐藏起来。2通过构建自动拆卸(第 503页)机制,我们可以消除为每个测试方法测试用例类编写手工拆卸代码的需要。它由部分组成:

Not surprisingly, this situation may inspire the lazy but creative programmer to come up with a way to automate the teardown logic. The important thing to note is that teardown code doesn't help us understand the test so it is better for it to remain hidden.2 We can eliminate the need to write hand-crafted teardown code for each Test Method or Testcase Class by building an Automated Teardown (page 503) mechanism. It consists of three parts:

  1. 一种经过充分测试的机制,用于迭代需要删除的对象列表并捕获/报告遇到的任何错误,同时确保尝试所有删除操作。
  2. A well-tested mechanism to iterate over a list of objects that need to be deleted and catch/report any errors it encounters while ensuring that all the deletions are attempted.
  3. 一种调度机制,用于调用适合要删除的对象类型的删除代码。此机制通常作为 Command [GOF]对象实现,该对象包装了每个要删除的对象,但也可以简单到调用delete对象本身的方法或使用switch基于对象类的语句。
  4. A dispatching mechanism that invokes the deletion code appropriate to the kind of object to be deleted. This mechanism is often implemented as a Command [GOF] object that wraps each object to be deleted, but could be as simple as calling a delete method on the object itself or using a switch statement based on the object's class.
  5. 一种注册机制,用于将新创建的对象(必要时进行适当包装)添加到要删除的对象列表中。
  6. A registration mechanism to add newly created objects (suitably wrapped if necessary) to the list of objects to be deleted.

一旦我们构建了自动拆卸机制,我们就可以简单地从创建方法第 415页)中调用注册方法,并从方法中调用清理方法tearDown。后一个操作可以在所有测试用例类都继承自的测试用例超类第 638页)中指定。我们甚至可以扩展此机制以删除 SUT 在执行时创建的对象。为此,我们在 SUT 中使用可观察的对象工厂(请参阅第 686页的依赖项查找),并让我们的测试用例超类将自己注册为对象创建的观察者[GOF] 。

Once we have built our Automated Teardown mechanism, we can simply invoke the registration method from our Creation Methods (page 415) and the cleanup method from the tearDown method. The latter operation can be specified in a Testcase Superclass (page 638) that all of our Testcase Classes inherit from. We can even extend this mechanism to delete objects created by the SUT as it is exercised. To do so, we use an observable Object Factory (see Dependency Lookup on page 686) inside the SUT and have our Testcase Superclass register itself as an Observer [GOF] of object creation.

数据库拆除

当我们的持久性Fresh Fixture完全构建在关系数据库中时,我们可以利用数据库的某些功能来实现它的拆卸。表截断拆卸第 661页)是一种使用单个数据库命令清除表的全部内容的强力方法。当然,它只适用于每个测试运行器(第377页)都有自己的数据库沙箱第 650页)的情况。一种不那么激烈的方法是使用事务回滚拆卸第 668页)撤消在当前测试上下文中所做的所有更改。此机制依赖于使用Humble 事务控制器模式设计的 SUT(请参阅第695页的Humble 对象),以便我们可以从测试中调用业务逻辑,而无需 SUT 自动提交事务。这两种特定于数据库的拆卸模式通常都使用隐式拆卸来实现,以将拆卸逻辑排除在测试方法之外。

When our persistent Fresh Fixture has been built entirely in a relational database, we can take advantage of certain features of the database to implement its teardown. Table Truncation Teardown (page 661) is a brute-force way to blow away the entire contents of a table with a single database command. Of course, it is appropriate only when each Test Runner (page 377) has its own Database Sandbox (page 650). A somewhat less drastic approach is to use Transaction Rollback Teardown (page 668) to undo all changes made within the context of the current test. This mechanism relies on the SUT having been designed using the Humble Transaction Controller pattern (see Humble Object on page 695) so that we can invoke the business logic from the test without having the SUT commit the transaction automatically. Both of these database-specific teardown patterns are most commonly implemented using Implicit Teardown to keep the teardown logic out of the Test Methods.

避免拆卸

Avoiding the Need for Teardown

到目前为止,我们已经了解了如何拆卸装置。现在,让我们看看如何避免拆卸装置。

So far, we have looked at ways to do fixture teardown. Now, let us look at ways to avoid fixture teardown.

避免装置碰撞

我们需要拆卸夹具有三个原因:

We need to do fixture teardown for three reasons:

  1. 剩余固定装置的对象的积累可能会导致测试运行缓慢。
  2. The accumulation of leftover fixture objects can cause tests to run slowly.
  3. 剩余的固定装置对象可能会导致 SUT 行为异常或我们的断言报告不正确的结果。
  4. The leftover fixture objects can cause the SUT to behave differently or our assertions to report incorrect results.
  5. 剩余的 Fixture 对象会阻止我们创建测试所需的Fresh Fixture 。
  6. The leftover fixture objects can prevent us from creating the Fresh Fixture required by our test.

最容易解决的问题是第一个问题:我们可以安排定期清理持久性机制,使其恢复到已知的最低限度状态。不幸的是,这种策略只有在我们能够在积累测试碎片的情况下让测试正确运行的情况下才有用。

The issue that is easiest to address is the first one: We can schedule a periodic cleansing of the persistence mechanism back to a known, minimalist state. Unfortunately, this tactic is useful only if we can get the tests to run correctly in the presence of accumulated test detritus.

第二个问题可以通过使用增量断言第 485页)而不是“绝对”断言来解决。增量断言的工作原理是在运行测试之前对装置进行快照,并在我们执行 SUT 之后验证是否出现了预期的差异。

The second issue can be addressed by using Delta Assertions (page 485) rather than "absolute" assertions. Delta Assertions work by taking a snapshot of the fixture before the test is run and verifying that the expected differences have appeared after we exercise the SUT.

第三个问题可以通过确保每个测试在每次运行时都生成一组不同的 Fixture 对象来解决。因此,测试需要创建的任何对象都必须具有完全唯一的标识符 — 即唯一的文件名、唯一的键等等。为此,我们可以构建一个简单的唯一 ID 生成器,并在每个测试开始时创建一个新 ID。然后,我们可以使用该唯一 ID 作为每个新创建的 Fixture 对象标识的一部分。如果 Fixture 在单个Test Runner之外共享,则可能需要在创建的唯一标识符中包含一些有关用户的信息;当前登录的用户 ID 通常就足够了。使用不同的生成值作为键还有另一个好处:它允许我们实现数据库分区方案(参见数据库沙箱),在该方案中,尽管存在剩余的 Fixture 对象,我们也可以使用绝对断言。

The third issue can be addressed by ensuring that each test generates a different set of fixture objects each time it is run. Thus any objects that the test needs to create must be given totally unique identifiers—that is, unique filenames, unique keys, and so on. To do so, we can build a simple unique ID generator and create a new ID at the beginning of each test. We can then use that unique ID as part of the identity of each newly created fixture object. If the fixture is shared beyond a single Test Runner, we may need to include something about the user in the unique identifiers we create; the currently logged-in user ID is usually sufficient. Using Distinct Generated Values as keys offers another benefit: It allows us to implement a Database Partitioning Scheme (see Database Sandbox) in which we can use absolute assertions despite the presence of leftover fixture objects.

避免 Fixture 持久化

我们似乎要费很大力气才能消除持久性Fresh Fixture所造成的副作用。如果可以避免所有这些工作,那不是很好吗?好消息是我们可以做到。坏消息是,为了做到这一点,我们需要将Fresh Fixture变为非持久性的。当 SUT 应该为 Fixture 的持久性负责时,一种可能性是用测试替身(第 522页)替换持久性机制,测试可以随意清除它。这种方法的一个很好的例子是使用伪数据库(请参见第551页的伪对象)。当测试应该为 Fixture 的持久性负责时,解决方案就更简单了:只需使用持久性较低的 Fixture 引用机制。

We seem to be going to a lot of trouble to undo the side effects caused by a persistent Fresh Fixture. Wouldn't it be nice if we could avoid all of this work? The good news is that we can. The bad news is that we need to make our Fresh Fixture nonpersistent to do so. When the SUT is to blame for the persistence of the fixture, one possibility is to replace the persistence mechanism with a Test Double (page 522) that the test can wipe out at will. A good example of this approach is the use of a Fake Database (see Fake Object on page 551). When the test is to blame for fixture persistence, the solution is even easier: Just use a less persistent fixture reference mechanism.

处理缓慢的测试

Dealing with Slow Tests

使用持久性新鲜夹具的一个主要缺点是速度,或者更准确地说,缺乏速度。文件系统和数据库比现代计算机中使用的处理器慢得多。因此,与数据库交互的测试往往比完全在内存中运行的测试运行得慢得多。这种差异的部分原因是 SUT 从磁盘访问夹具 - 但这个问题只是导致速度变慢的一小部分原因。在每个测试开始时设置新鲜夹具并在每个测试结束时将其拆除通常需要比 SUT 访问夹具更多的磁盘访问。因此,在其他所有条件相同的情况下,访问数据库的测试通常比完全在内存中运行的测试花费 50 到 100 倍3的时间来运行。

A major drawback of using a Persistent Fresh Fixture is speed or, more precisely, the lack thereof. File systems and databases are much slower than the processors used in modern computers. As a consequence, tests that interact with databases tend to run much more slowly than tests that run entirely in memory. Part of this difference arises because the SUT is accessing the fixture from disk—but this issue turns out to be only a small part of the reason for the slowdown. Setting up the Fresh Fixture at the beginning of each test and tearing it down at the end of each test typically takes many more disk accesses than those used by the SUT to access the fixture. As a result, tests that access the database often take 50 to 100 times3 longer to run than tests that run entirely in memory, all other things being equal.

针对持久性新装置导致的测试缓慢的典型反应是通过在多个测试中重复使用装置来消除装置设置和拆除开销。假设我们有五次磁盘访问来设置和拆除装置,对于 SUT 执行的每次磁盘访问,我们通过切换到共享装置所能做到的绝对最佳4 次,速度大约是原来的十倍。当然,在大多数情况下,这种结果仍然太慢,而且代价高昂:测试不再独立。这意味着我们可能会在慢速测试之上进行交互测试(参见不稳定测试)、孤立测试(参见不稳定测试)和不可重复测试

The typical reaction to slow tests caused by Persistent Fresh Fixtures is to eliminate the fixture setup and teardown overhead by reusing the fixture across many tests. Assuming we have five disk accesses to set up and tear down the fixture for every disk access performed by the SUT, the absolute best4 we can do by switching to a Shared Fixture is somewhere around ten times as slow. Of course, this outcome is still too slow in most situations and it comes with a hefty price: The tests are no longer independent. That means we will likely have Interacting Tests (see Erratic Test), Lonely Tests (see Erratic Test), and Unrepeatable Tests on top of our Slow Tests!

一个更好的解决方案是消除应用程序下基于磁盘的数据库的需求。只需付出一点努力,我们就能够用内存数据库(参见Fake Object)或Fake Database 替换基于磁盘的数据库。最好在项目早期就做出这个决定,因为此时工作量还很少。是的,有一些挑战,比如处理存储过程,但它们都是可以克服的。

A much better solution is to eliminate the need to have a disk-based database under the application. With a small amount of effort, we should be able to replace the disk-based database with an In-Memory Database (see Fake Object) or a Fake Database. This decision is best made early in the project while the effort is still low. Yes, there are some challenges, such as dealing with stored procedures, but they are all surmountable.

当然,这种策略并不是处理慢速测试的唯一方法。侧栏“不使用共享装置进行更快的测试”(第319页)探讨了一些其他策略。

This tactic isn't the only way to deal with Slow Tests, of course. The sidebar "Faster Tests Without Shared Fixtures" (page 319) explores some other strategies.

管理共享装置

Managing Shared Fixtures

管理共享 Fixture和管理持久性新鲜 Fixture 有很多共同之处只是我们故意选择不在每次测试后拆除 Fixture,以便可以在后续测试中重复使用它(图 9.2)。这意味着两件事。首先,我们必须能够在其他测试中访问 Fixture。其次,我们必须有一种方法来触发 Fixture 的构造和拆除。

Managing Shared Fixtures has a lot in common with managing Persistent Fresh Fixtures, except that we deliberately choose not to tear the fixture down after every test so that we can reuse it in subsequent tests (Figure 9.2). This implies two things. First, we must be able to access the fixture in the other tests. Second, we must have a way of triggering both the construction and the teardown of the fixture.

图 9.2. 共享装置,有两个测试方法共享该装置。共享装置设置一次,并由两个或多个测试使用,因此这些测试可能会有意或无意地相互作用。请注意,第二个测试缺少装置设置阶段。

Figure 9.2. A Shared Fixture with two Test Methods that share it. A Shared Fixture is set up once and used by two or more tests that may interact, either deliberately or accidentally, as a result. Note the lack of a fixture setup phase for the second test.

图像

访问共享装置

Accessing Shared Fixtures

无论我们如何以及何时选择构建共享装置,测试都需要一种方法来找到它们要重用的测试装置。我们可用的选择取决于装置的性质。当装置存储在数据库中(共享装置最常见的用法)时,只要测试知道数据库,就可以直接访问它,而无需直接引用装置对象。在数据库查找中,可能倾向于使用硬编码值(请参阅第 714页的文字值)来访问装置对象。但这几乎总是一个坏主意,因为它会导致测试和装置实现之间的紧密耦合,并且文档价值很差(模糊测试第 186页)。为了避免这些潜在问题,我们可以使用带有意图揭示名称[SBPP]的查找器方法(请参阅测试实用程序方法)来访问装置。这些查找器方法的名称可能与创建方法的名称非常相似,但它们返回对现有装置对象的引用,而不是构建全新的装置对象。

Regardless of how and when we choose to build the Shared Fixture, the tests need a way to find the test fixture they are to reuse. The choices available to us depend on the nature of the fixture. When the fixture is stored in a database (the most common usage of a Shared Fixture), tests may access it directly without making direct references to the fixture objects as long as they know about the database. There may be a temptation to use Hard-Coded Values (see Literal Value on page 714) in database lookups to access the fixture objects. This is almost always a bad idea, however, because it leads to a close coupling between tests and the fixture implementation and because it has poor documentation value (Obscure Test; page 186). To avoid these potential problems, we can use Finder Methods (see Test Utility Method) with Intent-Revealing Names [SBPP] to access the fixture. These Finder Methods may have names that are very similar to those of Creation Methods, but they return references to existing fixture objects rather than building brand new ones.

当 Fixture 存储在内存中时,我们有一系列可能的解决方案。如果所有需要共享 Fixture 的测试都在同一个Testcase Class中,我们可以使用Fixture 保存类变量来保存对 Fixture 的引用。只要我们给变量一个意图揭示名称,测试读取器就应该能够理解测试的先决条件。另一种选择是使用Finder 方法

We have a range of possible solutions when the fixture is stored in memory. If all tests that need to share the fixture are in the same Testcase Class, we can use a fixture holding class variable to hold the reference to the fixture. As long as we give the variable an Intent-Revealing Name, the test reader should be able to understand the pre-conditions of the test. Another alternative is to use a Finder Method.

如果我们需要在许多测试用例类之间共享装置,则必须使用更复杂的技术。当然,我们可以让一个类声明保存装置类变量,并让其他测试通过该变量访问装置。不幸的是,这种方法可能会在测试之间产生不必要的耦合。另一种选择是将声明移到众所周知的对象 - 即测试装置注册表(请参阅第643页的测试助手)。这个注册表[PEAA]对象可以是类似测试数据库的东西,也可以仅仅是一个类。它可以通过保存类变量的离散装置或通过Finder 方法公开装置的各个部分。当测试装置注册表只有知道如何访问对象但不保存对它们的引用的Finder 方法时,我们称之为测试助手

If we need to share the fixture across many Testcase Classes, we must use a more sophisticated technique. We could, of course, let one class declare the fixture holding class variable and have the other tests access the fixture via that variable. Unfortunately, this approach may create unnecessary coupling between the tests. Another alternative is to move the declaration to a well-known object—namely, a Test Fixture Registry (see Test Helper on page 643). This Registry [PEAA] object could be something like a test database or it could merely be a class. It can expose various parts of a fixture via discrete fixture holding class variables or via Finder Methods. When the Test Fixture Registry has only Finder Methods that know how to access the objects but don't hold references to them, we call it a Test Helper.

触发共享装置构造

Triggering Shared Fixture Construction

要共享测试装置,必须在任何测试方法需要它之前构建它。此构建可以最晚在测试方法逻辑运行之前进行,也可以在运行整个测试套件之前进行,或者在更早的某个时间进行(图 9.3)。这引出了共享装置创建的基本模式。

For a test fixture to be shared, it must be built before any Test Method needs it. This construction could take place as late as right before the Test Method's logic is run, just before the entire test suite is run, or at some earlier time (Figure 9.3). This leads us to the basic patterns of Shared Fixture creation.

图 9.3。 管理共享装置的方法多种多样。共享装置可以在不同时间设置;具体设置取决于有多少测试需要重用装置以及需要重用多少次。

Figure 9.3. The plethora of ways to manage a Shared Fixture. A Shared Fixture can be set up at a variety of times; the decision is based on how many tests need to reuse the fixture and how many times they need to do so.

图像

如果我们乐意在任何测试第一次需要时就创建测试装置,我们可以使用相应测试用例类的方法中的延迟设置第 435页)在运行第一个测试时创建它。后续测试将看到装置已经存在并重新使用它。由于没有明显的信号表明测试套件(或套件套件;请参阅第 387页的测试套件对象)中的最后一个测试已经运行,我们不知道每次测试运行后何时拆除装置。这可能导致不可重复的测试,因为装置可能会在测试运行之间存活(取决于各个测试如何访问它)。setUp

If we are happy with the idea of creating the test fixture the first time any test needs it, we can use Lazy Setup (page 435) in the setUp method of the corresponding Testcase Class to create it as part of running the first test. Subsequent tests will then see that the fixture already exists and reuse it. Because there is no obvious signal that the last test in a test suite (or Suite of Suites; see Test Suite Object on page 387) has been run, we won't know when to tear down the fixture after each test run. This can lead to Unrepeatable Tests because the fixture may survive across test runs (depending on how the various tests access it).

如果我们需要更广泛地共享 Fixture,我们可以在测试套件的开头包含一个Fixture Setup 测试用例。这是Chained Tests的一个特例,它与Lazy Setup存在同样的问题— 具体来说,我们不知道何时该拆除 Fixture。它还取决于套件内测试的顺序,因此它最适合与Test Enumeration第 399页)配合使用。

If we need to share the fixture more broadly, we could include a Fixture Setup Testcase at the beginning of the test suite. This is a special case of Chained Tests and suffers from the same problem as Lazy Setup—specifically, we don't know when it is time to tear down the fixture. It also depends on the ordering of tests within a suite, so it works best with Test Enumeration (page 399).

如果我们需要在运行测试套件之后拆除测试夹具,则必须使用夹具管理机制来告诉我们最后一个测试何时运行。 xUnit 家族的几个成员都支持setUp只为从单个Testcase Class创建的测试套件运行一次的方法的概念。此Suite Fixture Setup第 441页)方法具有相应的方法,该方法在最后一个测试方法运行完成tearDown时调用。5然后,我们可以保证为每次测试运行构建一个新的夹具。夹具不会遗留而导致后续测试运行出现问题,从而可以避免不可重复的测试;但它不能防止测试运行中的交互测试。可以将此功能作为扩展添加到 xUnit 家族的任何成员。当不支持此功能或我们需要在单个Testcase Class 之外共享夹具时,我们可以求助于使用Setup Decorator(第447页)将测试套件的运行与夹具和逻辑的执行括在一起。Setup Decorator的最大缺点是依赖于装饰器的测试不能单独运行;它们是孤独测试setUptearDown

If we need to be able to tear down the test fixture after running a test suite, we must use a fixture management mechanism that tells us when the last test has been run. Several members of the xUnit family support the concept of a setUp method that runs just once for the test suite created from a single Testcase Class. This Suite Fixture Setup (page 441) method has a corresponding tearDown method that is called when the last Test Method has finished running.5 We can then guarantee that a new fixture is built for each test run. The fixture is not left over to cause problems with subsequent test runs, which prevents Unrepeatable Tests; it does not prevent Interacting Tests within the test run, however. This capability could be added as an extension to any member of the xUnit family. When it isn't supported or when we need to share the fixture beyond a single Testcase Class, we can resort to using a Setup Decorator (page 447) to bracket the running of a test suite with the execution of the fixture setUp and tearDown logic. The biggest drawback of Setup Decorator is that tests that depend on the decorator cannot be run by themselves; they are Lonely Tests.

最后一种选择是在测试运行之前就构建好夹具,即使用预建夹具(第 429页)。这种方法提供了关于测试夹具实际构建方式的最多选项,因为夹具设置不需要在 xUnit 内部执行。例如,可以通过使用数据库脚本、复制“黄金”数据库或运行数据生成程序来手动设置。预建夹具的主要缺点是,如果任何测试是不可重复的测试,我们将需要在每次测试运行之前执行手动干预(第250页)。因此,预建夹具通常与新夹具结合使用,以构建不可变共享夹具(请参阅共享夹具)。

The final option is to build the fixture well before the tests are run—that is, to employ a Prebuilt Fixture (page 429). This approach offers the most options regarding how the test fixture is actually constructed because the fixture setup need not be executable from within xUnit. For example, it could be set up manually, by using database scripts, by copying a "golden" database, or by running a data generation program. The major disadvantage with a Prebuilt Fixture is that if any tests are Unrepeatable Tests, we will need to perform a Manual Intervention (page 250) before each test run. As a result, a Prebuilt Fixture is often used in combination with a Fresh Fixture to construct an Immutable Shared Fixture (see Shared Fixture).

下一步是什么?

What's Next?

现在我们已经确定了如何设置和拆除我们的装置,我们准备将注意力转向使用 SUT 并通过调用断言方法来验证是否发生了预期的结果。这个过程在第 10 章结果验证”中进行了更详细的描述。

Now that we've determined how we will set up and tear down our fixtures, we are ready to turn our attention to exercising the SUT and verifying that the expected outcome has occurred using calls to Assertion Methods. This process is described in more detail in Chapter 10, Result Verification.

第十章

结果验证

Chapter 10

Result Verification

 

关于本章

About This Chapter

第 8 章瞬态夹具管理”第 9 章持久夹具管理”介绍了如何设置测试夹具以及如何在执行 SUT 后将其拆除。本章介绍了用于验证 SUT 是否正确运行的各种选项,包括执行 SUT 并将实际结果与预期结果进行比较。

Chapter 8, Transient Fixture Management, and Chapter 9, Persistent Fixture Management, described how to set up the test fixture and how to tear it down after exercising the SUT. This chapter introduces a variety of options for verifying that the SUT has behaved correctly, including exercising the SUT and comparing the actual outcome with the expected outcome.

让测试自我检查

Making Tests Self-Checking

使用 xUnit 实现的自动化测试的一个关键特性是它们可以是(也应该是)自检测试(请参阅第21页的测试自动化目标)。这一特性使其足够划算,可以非常频繁地运行。xUnit 系列的大多数成员都带有一组内置的断言方法(第362页)和一些文档来告诉我们何时使用哪种方法。从表面上看,这听起来很简单 - 但是编写好的测试不仅仅是调用内置的断言方法。我们还需要学习使测试易于理解以及避免和消除测试代码重复第 213页)的关键技术。

One of the key characteristics of tests automated using xUnit is that they can be (and should be) Self-Checking Tests (see Goals of Test Automation on page 21). This characteristic makes them cost-effective enough to be run very frequently. Most members of the xUnit family come with a collection of built-in Assertion Methods (page 362) and some documentation that tells us which one to use when. On the surface this sounds pretty simple—but there's a lot more to writing good tests than just calling the built-in Assertion Methods. We also need to learn key techniques for making tests easy to understand and for avoiding and removing Test Code Duplication (page 213).

编写断言时面临的一个关键挑战是获取我们想要与预期结果进行比较的信息。这就是观察点发挥作用的地方;它们提供了一个了解 SUT 状态或行为的窗口,以便我们可以将其传递给断言方法。通过同步方法调用访问的信息的观察点相对简单;其他类型信息的观察点可能非常具有挑战性,这正是自动化单元测试如此有趣的原因。

A key challenge in coding the assertions is getting access to the information we want to compare with the expected results. This is where observation points come into play; they provide a window into the state or behavior of the SUT so that we can pass it to the Assertion Methods. Observation points for information accessible via synchronous method calls are relatively straightforward; observation points for other kinds of information can be quite challenging, which is precisely what makes automated unit testing so interesting.

断言通常(但不总是)在 SUT 执行之后立即从测试方法(第 348页) 主体中调用。一些测试自动化程序将断言放在测试的夹具设置阶段之后,以确保夹具设置正确。这种做法几乎总是会导致模糊测试(第 186页),因此我宁愿为测试实用程序方法(第 599页) 编写单元测试。1有些测试风格确实要求我们在执行 SUT之前设置我们的期望;这个主题将在第 11 章“使用测试替身”中详细讨论。在本章中,我们将看到几个从测试实用程序方法中调用断言方法的示例。

Assertions are usually—but not always—called from within the Test Method (page 348) body right after the SUT has been exercised. Some test automaters put assertions after the fixture setup phase of the test to ensure that the fixture is set up correctly. This practice almost always contributes to Obscure Tests (page 186), so I would rather write unit tests for the Test Utility Methods (page 599).1 Some styles of testing do require us to set up our expectations before we exercise the SUT; this topic is discussed in more detail in Chapter 11, Using Test Doubles. We'll see several examples of calling Assertion Methods from within Test Utility Methods in this chapter.

一个可能(尽管很少使用)的地方是将断言方法调用放在Implicit Teardown第 516tearDown页)中使用的方法中。因为此方法会针对每个测试运行,无论该测试是通过还是失败(只要方法成功),都可以在此处放置断言。此方案涉及与使用Implicit Setup(第424页)构建测试装置相同的权衡;它不太明显,但会自动完成。请参阅侧栏“使用增量断言检测数据泄漏”(第487页)中的示例,该示例将断言放在超类的Implicit Teardown所使用的方法中,以检测测试何时在数据库中留下剩余的测试对象。setUptearDown

One possible—though rarely used—place to put calls to Assertion Methods is in the tearDown method used in Implicit Teardown (page 516). Because this method is run for every test, whether that test passed or failed (as long as the setUp method succeeded), one can put assertions here. This scheme involves the same trade-off as using Implicit Setup (page 424) for building our test fixture; it's less visible but done automatically. See the sidebar "Using Delta Assertions to Detect Data Leakage" (page 487) for an example of putting assertions in the tearDown method used by Implicit Teardown of a superclass to detect when tests leave leftover test objects in the database.

验证状态还是行为?

Verify State or Behavior?

归根结底,测试自动化就是验证 SUT 的行为。SUT 行为的某些方面可以直接验证;函数返回的值就是一个很好的例子。行为的其他方面更容易通过查看某个对象的状态来间接验证。我们可以通过两种方式在测试中验证 SUT 的实际行为:

Ultimately, test automation is about verifying the behavior of the SUT. Some aspects of the SUT's behavior can be verified directly; the value returned by a function is a good example. Other aspects of the behavior are more easily verified indirectly by looking at the state of some object. We can verify the actual behavior of the SUT in our tests in two ways:

  1. 我们可以通过使用观察点提取每个状态并使用断言将其与预期状态进行比较来验证受 SUT 影响的各种对象的状态。
  2. We can verify the states of various objects affected by the SUT by extracting each state using an observation point and using assertions to compare it to the expected state.
  3. 我们可以通过在 SUT 和其依赖组件 (DOC) 之间插入观察点来监视其交互(以其进行的方法调用的形式),并将这些方法调用与我们的预期进行比较,从而直接验证 SUT 的行为。
  4. We can verify the behavior of the SUT directly by using observation points inserted between the SUT and its depended-on component (DOC) to monitor its interactions (in the form of the method calls it makes) and comparing those method calls with what we expected.

状态验证第 462)使用断言完成,是两种方法中较简单的一种。行为验证(第468)更为复杂,建立在我们用于验证状态的断言技术之上。

State Verification (page 462) is done using assertions and is the simpler of the two approaches. Behavior Verification (page 468) is more complicated and builds on the assertion techniques we use for verifying state.

州验证

State Verification

验证预期结果是否发生的“正常”方法称为状态验证图 10.1)。首先,我们运行 SUT;然后使用断言检查 SUT 的运行后状态。我们还可以检查 SUT 因我们为运行它而进行的方法调用而返回的任何内容。最值得注意的是我们不做的事情:我们不以任何方式检测 SUT 以检测它如何与系统的其他组件交互。也就是说,我们只检查直接输出,并且我们只使用直接方法调用作为观察点。

The "normal" way to verify the expected outcome has occurred is called State Verification (Figure 10.1). First we exercise the SUT; then we examine the post-exercise state of the SUT using assertions. We may also examine anything returned by the SUT as a result of the method call we made to exercise it. What is most notable is what we do not do: We do not instrument the SUT in any way to detect how it interacts with other components of the system. That is, we inspect only direct outputs and we use only direct method calls as our observation points.

图 10.1。 状态验证。在状态验证中,我们断言,在执行完 SUT 后,SUT 及其返回的任何对象都处于预期状态。我们“不关注幕后的人”。

Figure 10.1. State Verification. In State Verification, we assert that the SUT and any objects it returns are in the expected state after we have exercised the SUT. We "pay no attention to the man behind the curtain."

图像

状态验证可以通过两种略有不同的方式完成。程序状态验证(参见状态验证)涉及编写一系列断言,这些断言将 SUT 的最终状态分开并验证其是否符合预期。预期对象(参见状态验证)是一种描述预期状态的方式,以便可以将其与单个断言方法调用进行比较;这种方法可以最大限度地减少测试代码重复并提高测试清晰度(本章后面将详细介绍)。对于这两种策略,我们可以使用“内置”断言或自定义断言(第474)。

State Verification can be done in two slightly different ways. Procedural State Verification (see State Verification) involves writing a sequence of assertions that pick apart the end state of the SUT and verify that it is as expected. Expected Object (see State Verification) is a way of describing the expected state in such a way that it can be compared with a single Assertion Method call; this approach minimizes Test Code Duplication and increases test clarity (more on this later in this chapter). With both strategies, we can use either "built-in" assertions or Custom Assertions (page 474).

使用内置断言

Using Built-in Assertions

我们使用测试框架提供的断言来指定应该是什么,并依靠它们来告诉我们什么时候不应该!但仅仅使用内置断言只是故事的一小部分。

We use the assertions provided by our testing framework to specify what should be and depend on them to tell us when it isn't so! But simply using the built-in assertions is only a small part of the story.

最简单的结果验证形式是断言,我们在其中指定什么应该是真的。xUnit 系列的大多数成员都支持一系列不同的断言方法,包括以下内容:

The simplest form of result verification is the assertion in which we specify what should be true. Most members of the xUnit family support a range of different Assertion Methods, including the following:

  • 陈述结果断言(参见断言方法),例如assertTrue  (aBooleanExpression)
  • Stated Outcome Assertions (see Assertion Method) such as assertTrue  (aBooleanExpression)
  • 简单的平等断言,例如assertEquals(expected,  actual)
  • Simple Equality Assertions such as assertEquals(expected,  actual)
  • 模糊相等断言,例如assertEquals(expected,  actual,  tolerance),用于比较浮点数
  • Fuzzy Equality Assertions such as assertEquals(expected,  actual,  tolerance), which are used for comparing floats

当然,测试编程语言对断言的性质有一定的影响。在 JUnit、SUnit、CppUnit、NUnit 和 CsUnit 中,大多数相等性断言都以一对Objects 作为参数。有些语言支持方法参数类型的“重载”,因此我们可以针对不同类型的对象使用不同的断言实现。有些语言(例如 C)不支持对象,因此我们无法比较对象,只能比较值。

Of course, the test programming language has some influence on the nature of the assertions. In JUnit, SUnit, CppUnit, NUnit, and CsUnit, most of the Equality Assertions take a pair of Objects as their parameters. Some languages support "overloading" of method parameter types so we can have different implementations of an assertion for different types of objects. Some languages—C, for example—don't support objects, so we cannot compare objects, only values.

使用断言方法时需要考虑几个问题。当然,首要任务是验证所有应该为真的事情。我们的断言越好,我们的安全网就越精细(参见第24页),我们对代码的信心就越高。第二个优先事项是断言的文档价值。每个测试都应该非常清楚地表明“当系统处于状态 S1 并且我执行 X 时,结果应该是 R,系统应该处于状态 S2。”我们在夹具设置逻辑中将系统置于状态 S1。“我执行 X”对应于测试的练习 SUT 阶段。“结果是 R”和“系统处于状态 S2”是使用断言实现的。因此,我们希望以简洁的方式编写断言,使它们能够描述“R”和“S2”。

There are several issues to consider when using Assertion Methods. Naturally, the first priority is the verification of all things that should be true. The better our assertions, the finer our Safety Net (see page 24) and the higher our confidence in our code. The second priority is the documentation value of the assertions. Each test should make it very clear that "When the system is in state S1 and I do X, the result should be R and the system should be in state S2." We put the system into state S1 in our fixture setup logic. "I do X" corresponds to the exercise SUT phase of the test. "The result is R" and "the system is in state S2" are implemented using assertions. Thus we want to write our assertions in such a way that they succinctly describe "R" and "S2."

另一件需要考虑的事情是,当测试失败时,我们希望失败消息能够告诉我们足够多的信息,以便我们识别问题。2因此,我们几乎总是应该将断言消息(第 370页) 作为可选参数(假设我们的 xUnit 系列成员有一个!)。这种策略避免了我们玩断言轮盘赌(第 224message页)的可能性,在这种情况下,如果不以交互方式运行测试,我们甚至无法判断哪个断言失败了;它使集成构建 [SCM] 失败更容易重现和修复。它还通过告诉我们应该发生什么,使对损坏的测试进行故障排除变得更容易;实际结果告诉我们发生了什么!

Another thing to consider is that when the test fails, we want the failure message to tell us enough to enable us to identify the problem.2 Therefore, we should almost always include an Assertion Message (page 370) as the optional message parameter (assuming our xUnit family member has one!). This tactic avoids the possibility of us playing Assertion Roulette (page 224), in which we cannot even tell which assertion is failing without running the test interactively; it makes Integration Build [SCM] failures much easier to reproduce and fix. It also makes troubleshooting broken tests easier by telling us what should have happened; the actual outcome tells us what did happen!

当我们使用声明结果断言(例如 JUnit 的assertTrue)时,失败消息往往没有帮助(例如,“断言失败”)。我们可以通过使用参数描述消息(参见断言消息)使断言输出更加具体,该消息通过将有用的数据位合并到消息中而构建。一个好的开始是将作为断言方法的参数传递的表达式中的每个值都包括在内。

When we use a Stated Outcome Assertion (such as JUnit's assertTrue), the failure messages tend to be unhelpful (e.g., "Assertion failed"). We can make the assertion output much more specific by using an Argument-Describing Message (see Assertion Message) constructed by incorporating useful bits of data into the message. A good start is to include each of the values in the expression passed as the Assertion Method's arguments.

增量断言

Delta Assertions

使用共享装置(第 317页) 时,我们可能会发现我们有交互测试(请参阅第228页的不稳定测试),因为每个测试都会向数据库中添加更多对象 / 行,并且我们永远无法确定在执行 SUT 之后应该到底应该有什么。处理这种不确定性的一种方法是使用Delta 断言(第 485页) 来仅验证新添加的对象 / 行。在这种方法中,我们在测试开始时对相关表 / 类进行某种“快照”;然后,我们从测试结束时生成的实际对象 / 行集合中删除这些表 / 类,然后再将它们与预期对象进行比较。虽然这种策略会给测试带来显著的额外复杂性,但增加的复杂性可以重构为自定义断言和 / 或验证方法(请参阅自定义断言)。如果所有设置都发生在调用测试方法之前[例如,隐式设置共享装置预建装置第 429页)],则可以在测试方法中或方法中以内联方式获取“之前”快照。setUp

When using a Shared Fixture (page 317), we may find that we have Interacting Tests (see Erratic Test on page 228) because each test adds more objects/rows into the database and we can never be certain exactly what should be there after the SUT has been exercised. One way to deal with this uncertainty is to use Delta Assertions (page 485) to verify only the newly added objects/rows. In this approach, we take some sort of "snapshot" of the relevant tables/classes at the beginning of the test; we then remove these tables/classes from the collection of actual objects/rows produced at the end of the test before comparing them to the Expected Objects. Although this tactic can introduce significant extra complexity into the tests, the added complexity can be refactored into Custom Assertions and/or Verification Methods (see Custom Assertion). The "before" snapshot may be taken on an in-line basis within the Test Method or in the setUp method if all setup occurs before the Test Method is invoked [e.g., Implicit Setup, a Shared Fixture, or a Prebuilt Fixture (page 429)].

外部结果验证

External Result Verification

到目前为止,我们仅描述了对预期结果进行常规的“内存中”验证。事实上,另一种方法也是可行的——将预期结果和实际结果存储在文件中,并使用外部比较程序报告任何差异。这实际上是一种自定义断言,它对两个文件引用使用“深度比较”。比较程序通常需要被告知要忽略文件的哪些部分(或者需要先删除这些部分),这实际上使其成为模糊相等断言

Thus far we have described only conventional "in-memory" verification of the expected results. In fact, another approach is possible—one that involves storing the expected and actual results in files and using an external comparison program to report on any differences. This is, in effect, a form of Custom Assertion that uses a "deep compare" on two file references. The comparison program often needs to be told which parts of the files to ignore (or these parts need to be stripped out first), effectively making this a Fuzzy Equality Assertion.

外部结果验证特别适合于对没有太大变化的应用程序进行回归测试的自动化验收测试。这种方法的主要缺点是,从测试读者的角度来看,我们几乎总是会遇到神秘客人(参见模糊测试),因为预期结果在测试中不可见。避免此问题的一种方法是让测试写入预期文件的内容,从而使测试读者可以看到这些内容。此步骤仅在数据量非常小的情况下才实用 - 这也是支持最小夹具第 302页)的另一个理由。

External result verification is particularly appropriate for automating acceptance tests for regression-testing an application that hasn't changed very much. The major disadvantage of this approach is that we almost always end up with a Mystery Guest (see Obscure Test) from the test reader's perspective because the expected results are not visible inside the test. One way to avoid this problem is to have the test write the contents of the expected file, thereby making the contents visible to the test reader. This step is practical only if the amount of data is quite small—another argument in favor of a Minimal Fixture (page 302).

验证行为

Verifying Behavior

验证行为比验证状态更复杂,因为行为是动态的。我们必须在 SUT 生成间接输出给其依赖的对象时“实时”捕获它(图 10.2)。两种基本的行为验证方式值得讨论:过程行为验证预期行为。两者都需要一种机制来访问 SUT 的传出方法调用(其间接输出)。第 11 章使用测试替身”更详细地介绍了测试替身的这种用法和其他用法(第522页) 。

Verifying behavior is more complicated than verifying state because behavior is dynamic. We have to catch the SUT "in the act" as it generates indirect outputs to the objects it depends on (Figure 10.2). Two basic styles of behavior verification are worth discussing: Procedural Behavior Verification and Expected Behavior. Both require a mechanism to access the outgoing method calls of the SUT (its indirect outputs). This and other uses of Test Doubles (page 522) are described in more detail in Chapter 11, Using Test Doubles.

图 10.2。 行为验证。在行为验证中,我们将断言重点放在 SUT 的间接输出(传出接口)上。这通常涉及用一些有助于观察和验证传出调用的东西替换 DOC。

Figure 10.2. Behavior Verification. In Behavior Verification, we focus our assertions on the indirect outputs (outgoing interfaces) of the SUT. This typically involves replacing the DOC with something that facilitates observing and verifying the outgoing calls.

图像

程序行为验证

Procedural Behavior Verification

程序行为验证中,我们捕获 SUT 执行时的行为并保存该数据以供日后检索。然后,测试将 SUT 的每个输出(逐一)与相应的预期输出进行比较。因此,在程序行为验证中,测试执行一个程序(一组步骤)来验证行为。

In Procedural Behavior Verification, we capture the behavior of the SUT as it executes and save that data for later retrieval. The test then compares each output of the SUT (one by one) with the corresponding expected output. Thus, in Procedural Behavior Verification, the test executes a procedure (a set of steps) to verify the behavior.

public void testRemoveFlightLogging_recordingTestStub()

               throws Exception {

      // 固定设置

      FlightDto expectedFlightDto = createAnUnregFlight();

      FlightManagementFacade Facade =

                new FlightManagementFacadeImpl();

      // 测试替身设置

      AuditLogSpy logSpy = new AuditLogSpy();

      Facade.setAuditLog(logSpy);

      // 练习

      Facade.removeFlight(expectedFlightDto.getFlightNumber());

      // 验证

     assertEquals("呼叫次数", 1,

                          logSpy.getNumberOfCalls());

     assertEquals("操作代码",

                          Helper.REMOVE_FLIGHT_ACTION_CODE,

                          logSpy.getActionCode());

     assertEquals("日期", helper.getTodaysDateWithoutTime(),

                          logSpy.getDate());

     assertEquals("用户", Helper.TEST_USER_NAME,

                          logSpy.getUser());

     断言Equals(“详细信息”,

                          expectedFlightDto.getFlightNumber(),

                          logSpy.getDetail());

}

public  void  testRemoveFlightLogging_recordingTestStub()

               throws  Exception  {

      //  fixture  setup

      FlightDto  expectedFlightDto  =  createAnUnregFlight();

      FlightManagementFacade  facade  =

                new  FlightManagementFacadeImpl();

      //        Test  Double  setup

      AuditLogSpy  logSpy  =  new  AuditLogSpy();

      facade.setAuditLog(logSpy);

      //  exercise

      facade.removeFlight(expectedFlightDto.getFlightNumber());

      //  verify

     assertEquals("number  of  calls",  1,

                          logSpy.getNumberOfCalls());

     assertEquals("action  code",

                          Helper.REMOVE_FLIGHT_ACTION_CODE,

                          logSpy.getActionCode());

     assertEquals("date",  helper.getTodaysDateWithoutTime(),

                          logSpy.getDate());

     assertEquals("user",  Helper.TEST_USER_NAME,

                          logSpy.getUser());

     assertEquals("detail",

                          expectedFlightDto.getFlightNumber(),

                          logSpy.getDetail());

}

 

程序行为验证中的关键挑战是捕获行为发生时的行为并将其保存,直到测试准备好使用此信息为止。此任务通过配置 SUT 以使用测试间谍(第538页)或自分流器(请参阅第568页的硬编码测试替身3而不是依赖类来完成。在执行 SUT 后,测试将检索行为记录并使用断言对其进行验证。

The key challenge in Procedural Behavior Verification is capturing the behavior as it occurs and saving it until the test is ready to use this information. This task is accomplished by configuring the SUT to use a Test Spy (page 538) or a Self Shunt (see Hard-Coded Test Double on page 568)3 instead of the depended-on class. After the SUT has been exercised, the test retrieves the recording of the behavior and verifies it using assertions.

预期行为规范

Expected Behavior Specification

如果我们可以构建一个预期对象并将其与 SUT 返回的实际对象进行比较以验证状态,那么我们可以做类似的事情来验证行为吗?是的,我们可以并且确实这样做了。预期行为通常与跨层测试结合使用,以验证对象或组件的间接输出。我们使用我们期望 SUT 对其执行的方法调用配置模拟对象第 544页),并在执行 SUT 之前安装此对象。

If we can build an Expected Object and compare it with the actual object returned by the SUT for verifying state, can we do something similar for verifying behavior? Yes, we can and do. Expected Behavior is often used in conjunction with layer-crossing tests to verify the indirect outputs of an object or component. We configure a Mock Object (page 544) with the method calls we expect the SUT to make to it and install this object before exercising the SUT.

public void testRemoveFlight_JMock() throws Exception {

      // 夹具设置

      FlightDto expectedFlightDto = createAnonRegFlight();

      FlightManagementFacade Facade =

                  new FlightManagementFacadeImpl();

      // 模拟配置

      Mock mockLog = mock(AuditLog.class);

      mockLog.expects(once()).method("logMessage")

                        .with(eq(helper.getTodaysDateWithoutTime()),

                                 eq(Helper.TEST_USER_NAME),

                                 eq(Helper.REMOVE_FLIGHT_ACTION_CODE),

                                 eq(expectedFlightDto.getFlightNumber()));

      // 模拟安装

      Facade.setAuditLog((AuditLog) mockLog.proxy());

      // 练习

      Facade.removeFlight(expectedFlightDto.getFlightNumber());

      // 验证

      // JMock 自动调用的 verify() 方法

}

public  void  testRemoveFlight_JMock()  throws  Exception  {

      //  fixture  setup

      FlightDto  expectedFlightDto  =  createAnonRegFlight();

      FlightManagementFacade  facade  =

                  new  FlightManagementFacadeImpl();

      //  mock  configuration

      Mock  mockLog  =  mock(AuditLog.class);

      mockLog.expects(once()).method("logMessage")

                        .with(eq(helper.getTodaysDateWithoutTime()),

                                 eq(Helper.TEST_USER_NAME),

                                 eq(Helper.REMOVE_FLIGHT_ACTION_CODE),

                                 eq(expectedFlightDto.getFlightNumber()));

      //  mock  installation

      facade.setAuditLog((AuditLog)  mockLog.proxy());

      //  exercise

      facade.removeFlight(expectedFlightDto.getFlightNumber());

      //  verify

      //  verify()  method  called  automatically  by  JMock

}

 

减少测试代码重复

Reducing Test Code Duplication

最常见的测试异味之一是测试代码重复。我们编写的每个测试都很有可能引入一些重复,尤其是当我们使用“剪切和粘贴”从现有测试创建新测试时。有些人会争辩说,测试代码中的重复并不像生产代码中的重复那么糟糕。如果测试代码重复 导致了其他异味,例如易碎测试(第239页)、易碎夹具(参见易碎测试高测试维护成本(第 265页),那么它就是糟糕的,因为太多测试与标准夹具第 305页)或 SUT 的 API 耦合过于紧密。此外,测试代码重复有时可能是另一个问题的征兆 - 即测试的意图被太多代码所掩盖(即模糊测试)。

One of the most common test smells is Test Code Duplication. With every test we write, there is a good chance we have introduced some duplication, but especially if we used "cut and paste" to create a new test from an existing test. Some will argue that duplication in test code is not nearly as bad as duplication in production code. Test Code Duplication is bad if it leads to some other smell such as Fragile Test (page 239), Fragile Fixture (see Fragile Test), or High Test Maintenance Cost (page 265) because too many tests are too closely coupled to the Standard Fixture (page 305) or the API of the SUT. In addition, Test Code Duplication may sometimes be a symptom of another problem—namely, the intent of the tests being obscured by too much code (i.e., an Obscure Test).

在结果验证逻辑中,测试代码重复通常表现为一组重复的断言。在这种情况下,有几种技术可以减少断言的数量:

In result verification logic, Test Code Duplication usually shows up as a set of repeated assertions. Several techniques are available to reduce the number of assertions in such cases:

预期对象

Expected Objects

我们经常会发现自己对同一对象的不同字段执行一系列断言。如果我们开始重复这组断言(无论是在单个测试中还是在多个测试中多次),我们应该寻找一种方法来减少测试代码重复。下一个列表显示了一个比较单个对象的多个属性的测试方法。许多其他测试方法可能需要相同的断言序列。

Often, we will find ourselves doing a series of assertions on different fields of the same object. If we begin repeating this group of assertions (whether multiple times in a single test or in multiple tests), we should look for a way to reduce the Test Code Duplication. The next listing shows one Test Method that compares several attributes of a single object. Many other Test Methods probably require the same sequence of assertions.

public void testInvoice_addLineItem7() {

     LineItem expItem = new LineItem(inv, product, QUANTITY);

      // 练习

      inv.addItemQuantity(product, QUANTITY);

      // 验证

      列表 lineItems = inv.getLineItems();

     LineItem actual = (LineItem)lineItems.get(0);

     assertEquals(expItem.getInv(), actual.getInv());

     assertEquals(expItem.getProd(), actual.getProd());

     assertEquals(expItem.getQuantity(), actual.getQuantity());

}

public  void  testInvoice_addLineItem7()  {

     LineItem  expItem  =  new  LineItem(inv,  product,  QUANTITY);

      //  Exercise

      inv.addItemQuantity(product,  QUANTITY);

      //  Verify

      List  lineItems  =  inv.getLineItems();

     LineItem  actual  =  (LineItem)lineItems.get(0);

     assertEquals(expItem.getInv(),  actual.getInv());

     assertEquals(expItem.getProd(),  actual.getProd());

     assertEquals(expItem.getQuantity(),  actual.getQuantity());

}

 

最明显的替代方案是使用单个相等性断言来比较两个整个对象,而不是使用许多相等性断言调用来逐个字段进行比较。如果值存储在单个变量中,我们可能需要创建相应类的新对象并使用这些值初始化其字段。只要我们有一个equals仅比较这些字段的方法并且我们能够随意创建预期对象,此技术就有效。

The most obvious alternative is to use a single Equality Assertion to compare two whole objects to each other rather than using many Equality Assertion calls to compare them field by field. If the values are stored in individual variables, we may need to create a new object of the appropriate class and initialize its fields with those values. This technique works as long as we have an equals method that compares only those fields and we have the ability to create the Expected Object at will.

public void testInvoice_addLineItem8() {

     LineItem expItem = new LineItem(inv, product, QUANTITY);

      // 练习

      inv.addItemQuantity(product, QUANTITY);

      // 验证

      列表 lineItems = inv.getLineItems();

     LineItem actual = (LineItem)lineItems.get(0);

     assertEquals("Item", expItem, actual);

}

public  void  testInvoice_addLineItem8()  {

     LineItem  expItem  =  new  LineItem(inv,  product,  QUANTITY);

      //  Exercise

      inv.addItemQuantity(product,  QUANTITY);

      //  Verify

      List  lineItems  =  inv.getLineItems();

     LineItem  actual  =  (LineItem)lineItems.get(0);

     assertEquals("Item",  expItem,  actual);

}

 

但是,如果我们不想比较对象中的所有字段,或者equals方法寻找的是身份而不是相等性,该怎么办?如果我们想要测试特定的相等性,该怎么办?如果由于不存在构造函数而无法创建预期对象的实例,该怎么办?在这种情况下,我们有两个选择:我们可以实现一个自定义断言,以我们想要的方式定义相等性,或者我们可以在传递给断言方法的预期对象equals的类的方法中实现测试特定的相等性。此类不必与实际对象的类相同;它只需实现将自身与实际对象类的实例进行比较。因此,它可以是一个简单的数据传输对象[CJ2EEP],也可以是实际(生产)类的测试特定子类(第579页),只是重写了方法。equalsequals

But what if we don't want to compare all the fields in an object or the equals method looks for identity rather than equality? What if we want test-specific equality? What if we cannot create an instance of the Expected Object because no constructor exists? In this scenario, we have two options: We can implement a Custom Assertion that defines equality the way we want it or we can implement our test-specific equality in the equals method of the class of the Expected Object we pass to the Assertion Method. This class doesn't need to be the same class as that of the actual object; it just needs to implement equals to compare itself with an instance of the actual object's class. Therefore, it can be a simple Data Transfer Object [CJ2EEP] or it can be a Test-Specific Subclass (page 579) of the real (production) class with just the equals method overridden.

一些测试自动化人员认为,我们在做出断言时不应该依赖equalsSUT 的方法,因为它可能会发生变化,从而导致依赖于此方法的测试失败(或遗漏重要差异)。我更愿意务实地做出这个决定。如果使用equalsSUT 提供的定义似乎合理,那么我就会这样做。如果我需要其他东西,我会定义自定义断言或特定于测试的预期对象类。我还会问自己,如果该方法以后发生变化,改变我的策略有多难equals。例如,在支持参数类型重载的静态类型语言(如 Java)中,我们可以添加使用不同参数类型的自定义断言,以便在使用特定类型时覆盖默认实现。如果更改equals在以后导致问题,通常可以很容易地改进此代码。

Some test automaters don't think we should ever rely on the equals method of the SUT when making assertions because it could change, thereby causing tests that depend on this method to fail (or to miss important differences). I prefer to be pragmatic about this decision. If it seems reasonable to use the equals definition supplied by the SUT, then I do so. If I need something else, I define a Custom Assertion or a test-specific Expected Object class. I also ask myself how hard it would be to change my strategy if the equals method should later change. For example, in statically typed languages that support parameter type overloading (such as Java), we can add a Custom Assertion that uses different parameter types to override the default implementation when specific types are used. This code can often be retrofitted quite easily if a change to equals causes problems at a later date.

自定义断言

Custom Assertions

自定义断言是我们自己编写的特定领域断言。自定义断言将验证结果的过程隐藏在声明性名称后面,使我们的结果验证逻辑更能揭示意图。它们还可以通过消除大量可能分散注意力的代码来防止模糊测试。将代码移入自定义断言的另一个好处是,现在可以通过编写自定义断言测试对断言逻辑进行单元测试(请参阅自定义断言)。断言不再是不可测试的测试代码(请参阅第 209页的难以测试的代码)!

A Custom Assertion is a domain-specific assertion we write ourselves. Custom Assertions hide the procedure for verifying the results behind a declarative name, making our result verification logic more intent-revealing. They also prevent Obscure Tests by eliminating of a lot of potentially distracting code. Another benefit of moving the code into a Custom Assertion is that the assertion logic can now be unit-tested by writing Custom Assertion Tests (see Custom Assertion). The assertions are no longer Untestable Test Code (see Hard-to-Test Code on page 209)!

静态 void assertLineItemsEqual(

                            String msg, LineItem exp, LineItem act) {

     assertEquals (msg+" Inv", exp.getInv(), act.getInv());

     assertEquals (msg+" Prod", exp.getProd(), act.getProd());

     assertEquals (msg+" Quan", exp.getQuantity(), act.getQuantity());

}

static  void  assertLineItemsEqual(

                            String    msg,  LineItem  exp,  LineItem  act)  {

     assertEquals  (msg+"  Inv",    exp.getInv(),  act.getInv());

     assertEquals  (msg+"  Prod",  exp.getProd(),  act.getProd());

     assertEquals  (msg+"  Quan",  exp.getQuantity(),  act.getQuantity());

}

 

有两种方法可以创建自定义断言:(1)重构现有的复杂测试代码以减少测试代码重复;(2)在编写测试时编写对不存在的断言方法的调用,然后在找到一组测试方法所需的自定义断言套件后,用适当的逻辑填充方法主体。后一种技术是一种很好的方法,可以提醒我们期望执行 SUT 的结果是什么,即使我们还没有编写代码来验证它。无论哪种方式,定义一组自定义断言都是创建用于指定测试的高级语言(参见第41页)的第一步。

There are two ways to create Custom Assertions: (1) by refactoring existing complex test code to reduce Test Code Duplication and (2) by coding calls to nonexistent Assertion Methods as we write tests and then filling in the method bodies with the appropriate logic once we land on the suite of Custom Assertions needed by a set of Test Methods. The latter technique is a good way of reminding ourselves what we expect the outcome of exercising the SUT to be, even though we haven't yet written the code to verify it. Either way, the definition of a set of Custom Assertions is the first step toward creating a Higher-Level Language (see page 41) for specifying our tests.

重构为自定义断言时,我们只需对重复的断言使用提取方法 [Fowler],并为新方法赋予一个意图揭示名称[SBPP]。我们将现有验证逻辑使用的对象作为参数传入,并包含断言消息以区分对同一断言方法的调用。

When refactoring to Custom Assertions, we simply use Extract Method [Fowler] on the repeated assertions and give the new method an Intent-Revealing Name [SBPP]. We pass in the objects used by the existing verification logic as arguments and include an Assertion Message to differentiate between calls to the same assertion method.

结果描述验证方法

Outcome-Describing Verification Method

另一种源于对测试代码进行无情重构的技术是“结果描述”验证方法。假设我们发现一组测试都具有相同的练习 SUT 和验证结果部分。只有设置部分对于每个测试都不同。如果我们对通用代码进行提取方法重构并为其赋予一个有意义的名称,我们只需要更少的代码,实现更易于理解的测试,并同时生成可测试的验证逻辑!如果这不是重构代码的值得的理由,那么我不知道还有什么理由。

Another technique that is born from ruthless refactoring of test code is the "outcome-describing" Verification Method. Suppose we find that a group of tests all have identical exercise SUT and verify outcome sections. Only the setup portion is different for each test. If we do an Extract Method refactoring on the common code and give it a meaningful name, we need less code, achieve more understandable tests, and produce testable verification logic all at the same time! If this isn't a worthwhile reason for refactoring code, then I don't know what else could be.

void assertInvoiceContainsOnlyThisLineItem(

                                                        Invoice inv,

                                                        LineItem expItem) {

      List lineItems = inv.getLineItems();

     assertEquals("项目数量", lineItems.size(), 1);

     LineItem actual = (LineItem)lineItems.get(0);

     assertLineItemsEqual("",expItem, actual);

}

void  assertInvoiceContainsOnlyThisLineItem(

                                                        Invoice  inv,

                                                        LineItem  expItem)  {

      List  lineItems  =  inv.getLineItems();

     assertEquals("number  of  items",  lineItems.size(),  1);

     LineItem  actual  =  (LineItem)lineItems.get(0);

     assertLineItemsEqual("",expItem,  actual);

}

 

验证方法自定义断言之间的主要区别在于后者仅做出断言,而前者还与 SUT 交互(通常是为了执行它)。另一个区别是自定义断言通常具有标准的相等性断言签名:assertSomething(message,  expected,  actual)。相反,验证方法可能具有完全任意的参数,因为它们需要将其他参数传递到 SUT 中。它们本质上介于自定义断言参数化测试之间(第607页)。

The major difference between a Verification Method and a Custom Assertion is that the latter only makes assertions, while the former also interacts with the SUT (typically for the purpose of exercising it). Another difference is that Custom Assertions typically have a standard Equality Assertion signature: assertSomething(message,  expected,  actual). In contrast, Verification Methods may have completely arbitrary parameters because they require additional parameters to pass into the SUT. They are, in essence, halfway between a Custom Assertion and a Parameterized Test (page 607).

参数化和数据驱动测试

Parameterized and Data-Driven Tests

我们甚至可以更进一步分解测试之间的共性。如果设置测试装置的逻辑相同,但使用不同的数据,我们可以将通用的装置设置、练习 SUT 和验证测试结果阶段提取到新的参数化测试方法中。这个参数化测试不会被测试自动化框架(第 298页) 自动调用,因为它需要参数;相反,我们为每个测试定义非常简单的测试方法,然后调用参数化测试并传入使该测试独一无二所需的数据。这些数据可能包括装置设置、练习 SUT 和相应的预期结果所需的数据。在以下测试中,方法generateAndVerifyHtml就是参数化测试

We can go even further in factoring out the commonality between tests. If the logic to set up the test fixture is the same but uses different data, we can extract the common fixture setup, exercise SUT, and verify outcome phases of the test into a new Parameterized Test method. This Parameterized Test is not called automatically by the Test Automation Framework (page 298) because it requires arguments; instead, we define very simple Test Methods for each test, which then call the Parameterized Test and pass in the data required to make this test unique. This data may include that required for fixture setup, exercising the SUT, and the corresponding expected result. In the following tests, the method generateAndVerifyHtml is the Parameterized Test.

def test_extref

        sourceXml = "<extref id='abc' />"

        expectedHtml = "<a href='abc.html'>abc</a>"

        generateAndVerifyHtml(sourceXml,expectedHtml,"<extref>")

end



def test_testterm_normal

     sourceXml = "<testterm id='abc'/>"

     expectedHtml = "<a href='abc.html'>abc</a>"

     generateAndVerifyHtml(sourceXml,expectedHtml,"<testterm>")

end



def test_testterm_plural

     sourceXml = "<testterms id='abc'/>"

     expectedHtml = "<a href='abc.html'>abcs</a>"

     generateAndVerifyHtml(sourceXml,expectedHtml,"<plural>")

end

def  test_extref

        sourceXml  =  "<extref  id='abc'  />"

        expectedHtml  =  "<a  href='abc.html'>abc</a>"

        generateAndVerifyHtml(sourceXml,expectedHtml,"<extref>")

end



def  test_testterm_normal

     sourceXml  =  "<testterm  id='abc'/>"

     expectedHtml  =  "<a  href='abc.html'>abc</a>"

     generateAndVerifyHtml(sourceXml,expectedHtml,"<testterm>")

end



def  test_testterm_plural

     sourceXml  =  "<testterms  id='abc'/>"

     expectedHtml  =  "<a  href='abc.html'>abcs</a>"

     generateAndVerifyHtml(sourceXml,expectedHtml,"<plural>")

end

 

数据驱动测试(第 288页) 中,测试用例是完全通用的并可由框架直接执行;它在执行时从测试数据文件中读取参数。可以将数据驱动测试想象成一个内外翻转的参数化测试:测试方法将测试特定数据传递给参数化测试数据驱动测试就是测试方法,它从文件读取测试特定数据。文件的内容是用于测试的高级语言;数据驱动测试方法是该语言的解释器[GOF] 。此方案是 Fit 测试的 xUnit 等同方案。以下用 Ruby 编写的代码示例显示了数据驱动测试方法的一个简单示例:

In a Data-Driven Test (page 288), the test case is completely generic and directly executable by the framework; it reads the arguments from a test data file as it executes. Think of a Data-Driven Test as a Parameterized Test turned inside out: A Test Method passes test-specific data to a Parameterized Test; a Data-Driven Test is the Test Method and reads the test-specific data from a file. The contents of the file are a Higher-Level Language for testing; the Data-Driven Test method is the Interpreter [GOF] of that language. This scheme is the xUnit equivalent of a Fit test. A simple example of a Data-Driven Test method is shown in this code sample written in Ruby:

def test_crossref

     executeDataDrivenTest "CrossrefHandlerTest.txt"

end



def executeDataDrivenTest filename

     dataFile = File.open(filename)

  dataFile.each_line do | line |

     desc, action, part2 = line.split(",")

        sourceXml, expectedHtml, leftOver = part2.split(",")

        if "crossref"==action.strip

            generateAndVerifyHtml sourceXml, expectedHtml, desc

        else # 新的“动词”作为 elsif 的

              report_error( "unknown action" + action.strip )

        end

    end

end

def  test_crossref

     executeDataDrivenTest  "CrossrefHandlerTest.txt"

end



def  executeDataDrivenTest  filename

     dataFile  =  File.open(filename)

  dataFile.each_line  do  |  line  |

     desc,  action,  part2  =  line.split(",")

        sourceXml,  expectedHtml,  leftOver  =  part2.split(",")

        if  "crossref"==action.strip

            generateAndVerifyHtml  sourceXml,  expectedHtml,  desc

        else  #  new  "verbs"  go  before  here  as  elsif's

              report_error(  "unknown  action"  +  action.strip  )

        end

    end

end

 

以下是数据驱动测试方法读取的逗号分隔的数据文件:

Here is the comma-delimited data file that the Data-Driven Test method reads:

ID、操作、SourceXml、ExpectedHtml

Extref、crossref、<extref id='abc'/>、<a href='abc.html'>abc</a>

TTerm、crossref、<testterm id='abc'/>、<a href='abc.html'>abc</a>

TTerms、crossref、<testterms id='abc'/>、<a href='abc.html'>abcs</a>

ID,        Action,          SourceXml,                  ExpectedHtml

Extref,crossref,<extref  id='abc'/>,<a  href='abc.html'>abc</a>

TTerm,crossref,<testterm  id='abc'/>,<a  href='abc.html'>abc</a>

TTerms,crossref,<testterms  id='abc'/>,<a  href='abc.html'>abcs</a>

 

避免条件测试逻辑

Avoiding Conditional Test Logic

测试中我们还想避免使用条件逻辑。条件测试逻辑第 200页)很糟糕,因为同一个测试在不同情况下的执行方式可能不同。条件测试逻辑降低了我们对测试的信任,因为测试方法中的代码是不可测试的测试代码。为什么这很重要?因为我们验证测试方法的唯一方法是手动编辑 SUT,使其产生我们想要检测到的错误。如果测试方法有许多路径,我们需要确保每条路径都编码正确。在测试中只有一条可能的执行路径不是简单得多吗?让我们看看为什么我们可能在测试中包含条件逻辑的一些原因:

Another thing we want to avoid in our tests is conditional logic. Conditional Test Logic (page 200) is bad because the same test may execute differently in different circumstances. Conditional Test Logic reduces our trust in the tests because the code in our Test Methods is Untestable Test Code. Why is this important? Because the only way we can verify our Test Method is to manually edit the SUT so that it produces the error we want to be detected. If the Test Method has many paths through it, we need to make sure each path is coded correctly. Isn't it so much simpler just to have only one possible execution path through the test? Let us look at some reasons why we might include conditional logic in our tests:

  • 我们不想执行某些断言,因为根据我们在测试的这个阶段已经发现的情况(通常是失败情况),它们的执行没有意义。
  • We don't want to execute certain assertions because their execution doesn't make sense given what we have already discovered at this point in the test (typically a failure condition).
  • 我们必须考虑到在将实际结果与预期结果进行比较时出现的各种情况。
  • We have to allow for various situations in the actual results that we are comparing to the expected results.
  • 我们尝试在几种不同的情况下重用一种测试方法(本质上是将几种测试合并为一种测试方法)。
  • We are trying to reuse a Test Method in several different circumstances (essentially merging several tests into a single Test Method).

在前两种情况下使用条件测试逻辑的问题在于,它会使代码难以阅读,并且可能会掩盖通过灵活测试重用测试方法的情况(请参阅条件测试逻辑)。最后一个“原因”只是一个坏主意,简单明了。重用测试逻辑的方法比尝试重用测试方法本身要好得多。我们已经在本章的其他地方(在减少测试代码重复中)看到了其中一些重用技术,我们将在本书的其他地方看到其他方法。只需说“不”!

The problem with using Conditional Test Logic in the first two cases is that it makes the code hard to read and may mask cases of reusing test methods via Flexible Tests (see Conditional Test Logic). The last "reason" is just a bad idea, plain and simple. There are much better ways of reusing test logic than trying to reuse the Test Method itself. We have already seen some of these reuse techniques elsewhere in this chapter (in Reducing Test Code Duplication), and we will see other ways elsewhere in this book. Just say "no"!

好消息是,从我们的测试中删除所有合法的条件测试逻辑的使用是相对简单的。

The good news is that it is relatively straightforward to remove all legitimate uses of Conditional Test Logic from our tests.

消除“if”语句

Eliminating "if" Statements

当我们不想执行断言,因为我们知道它会导致测试错误,并且我们希望得到更有意义的测试失败消息时,我们该怎么办?正常的反应是将断言放在“if”语句中,如下面的清单所示。不幸的是,这种方法会导致条件测试逻辑,我们非常希望避免这种情况,因为我们希望每次运行测试时都运行完全相同的代码。

What should we do when we don't want to execute an assertion because we know it will result in a test error and we would prefer to have a more meaningful test failure message? The normal reaction is to place the assertion inside an "if" statement, as shown in the following listing. Unfortunately, this approach results in Conditional Test Logic, which we would dearly like to avoid because we want exactly the same code to run each time we run the test.

列表 lineItems = invoice.getLineItems();

if (lineItems.size() == 1) {

     LineItem expected =

          new LineItem(invoice, product,5,

                                  new BigDecimal("30"),

                                  new BigDecimal("69.96"));

     LineItem actItem = (LineItem) lineItems.get(0);

     assertEquals("invoice", expected, actItem);

} else {

      fail("发票应该只有一个行项目");

}

List  lineItems  =  invoice.getLineItems();

if  (lineItems.size()  ==  1)  {

     LineItem  expected  =

          new  LineItem(invoice,  product,5,

                                  new  BigDecimal("30"),

                                  new  BigDecimal("69.96"));

     LineItem  actItem  =  (LineItem)  lineItems.get(0);

     assertEquals("invoice",  expected,  actItem);

}  else  {

      fail("Invoice  should  have  exactly  one  line  item");

}

 

首选解决方案是使用Guard Assertion第 490页),如该测试代码的修订版本所示:

The preferred solution is to use a Guard Assertion (page 490) as shown in this revised version of the test code:

列表 lineItems = invoice.getLineItems();

assertEquals("项目数量", lineItems.size(), 1);

LineItem expected =

      new LineItem(invoice, product, 5,

                              new BigDecimal("30"),

                              new BigDecimal("69.96"));

LineItem actItem = (LineItem) lineItems.get(0);

assertEquals("发票", expected, actItem);

List  lineItems  =  invoice.getLineItems();

assertEquals("number  of  items",  lineItems.size(),  1);

LineItem  expected  =

      new  LineItem(invoice,  product,  5,

                              new  BigDecimal("30"),

                              new  BigDecimal("69.96"));

LineItem  actItem  =  (LineItem)  lineItems.get(0);

assertEquals("invoice",  expected,  actItem);

 

Guard Assertions的优点在于,它们可以防止我们遇到会导致测试错误的断言,而无需引入条件测试逻辑。一旦我们习惯了它们,这些断言就相当明显和直观。我们甚至可能发现自己想要在生产代码中断言方法的先决条件!

The nice thing about Guard Assertions is that they keep us from hitting the assertion that would cause a test error but without introducing Conditional Test Logic. Once we get used to them, these assertions are fairly obvious and intuitive to read. We may even find ourselves wanting to assert the pre-conditions of our methods in our production code!

消除环路

Eliminating Loops

条件测试逻辑也可能以循环的形式出现,用于验证 SUT 返回的集合内容是否符合我们的预期。将循环直接放入测试方法中会产生三个问题:

Conditional Test Logic may also appear as loops that verify the content of a collection returned by the SUT matches what we expected. Putting loops directly into the Test Method creates three problems:

  • 它引入了不可测试的测试代码,因为作为测试一部分的循环代码无法用全自动测试进行测试(参见第26页)。
  • It introduces Untestable Test Code because the looping code, which is part of the test, cannot be tested with Fully Automated Tests (see page 26).
  • 这会导致模糊测试,因为所有循环代码都掩盖了真实意图:集合是否匹配?
  • It leads to Obscure Tests because all that looping code obscures the real intent: Does or doesn't the collection match?
  • 这可能会导致项目级别的问题“开发人员不写测试”第 263页),因为编写循环的复杂性可能会阻止开发人员编写自检测试
  • It can lead to the project-level smell Developers Not Writing Tests (page 263) because the complexity of writing the loops may discourage the developer from writing the Self-Checking Test.

更好的解决方案是将此逻辑委托给具有意图揭示名称的测试实用程序方法,该方法既可测试又可重用。

A better solution is to delegate this logic to a Test Utility Method with an Intent-Revealing Name, which can be both tested and reused.

其他技术

Other Techniques

本节概述了编写易于理解的测试的一些其他技术。

This section outlines some other techniques for writing easy-to-understand tests.

从外向内逆向工作

Working Backward, Outside-In

编写非常能揭示意图的代码的一个有用小技巧是逆向工作。这是 Stephen Covey 思想的一个应用,“以终为始”。为此,我们首先编写函数或测试的最后一行。对于函数,其存在的全部原因是返回一个值;对于过程,它通过修改某些内容来产生一个或多个副作用。对于测试,存在的理由是验证预期结果是否已经发生(通过做出断言)。

A useful little trick for writing very intent-revealing code is to work backward. This is an application of Stephen Covey's idea, "Start with the end in mind." To do so, we write the last line of the function or test first. For a function, its whole reason for existence is to return a value; for a procedure, it is to produce one or more side effects by modifying something. For a test, the raison d' tre is to verify that the expected outcome has occurred (by making assertions).

逆向工作意味着我们首先编写这些断言。我们对适当命名的局部变量的值进行断言,以确保断言能够揭示意图。编写测试的其余部分只是填写执行这些断言所需的任何内容:我们声明变量来保存断言参数,并用适当的内容初始化它们。因为应该从 SUT 中检索至少一个参数,所以我们当然必须调用 SUT。为此,我们可能需要一些变量用作 SUT 参数。在使用变量后声明和初始化变量迫使我们在引入变量时更好地理解它。这种方案还可以产生更好的变量名,并避免使用像invoice1和 这样的无意义的名称invoice2

Working backward means we write these assertions first. We assert on the values of suitably named local variables to ensure that the assertion is intent-revealing. The rest of writing the test simply consists of filling in whatever is needed to execute those assertions: We declare variables to hold the assertion arguments and initialize them with the appropriate content. Because at least one argument should have been retrieved from the SUT, we must, of course, invoke the SUT. To do so, we may need some variables to use as SUT arguments. Declaring and initializing a variable after it has been used forces us to understand the variable better when we introduce it. This scheme also results in better variable names and avoids meaningless names like invoice1 and invoice2.

“由外而内”(有时也称“自上而下”)的工作意味着保持一致的抽象级别。测试方法应关注我们需要具备哪些条件才能在 SUT 中引发相关行为。如何到达该位置的机制应委托给测试软件的“较低层”。实际上,我们将此行为编码为对测试实用程序方法的调用,这使我们在编写每个测试方法时能够专注于 SUT 的要求。我们不必担心如何创建该对象或验证该结果;我们只需要描述该对象或结果应该是什么。我们刚刚使用但尚未定义的实用程序方法充当未完成的测试自动化逻辑的占位符。4我们可以继续编写此 SUT 所需的其他测试,趁它们还记忆犹新。稍后,我们可以切换到我们的“工具匠”帽子并实现测试实用程序方法

Working "outside-in" (or "top-down" as it is sometimes called) means staying at a consistent level of abstraction. The Test Method should focus on what we need to have in place to induce the relevant behavior in the SUT. The mechanics of how we reach that place should be delegated to a "lower layer" of test software. In practice, we code this behavior as calls to Test Utility Methods, which allows us to stay focused on the requirements of the SUT as we write each Test Method. We don't need to worry about how we will create that object or verify that outcome; we merely need to describe what that object or outcome should be. The utility method we just used but haven't yet defined acts as a placeholder for the unfinished test automation logic.4 We can move on to writing the other tests we need for this SUT while they are still fresh in our minds. Later, we can switch to our "toolsmith" hat and implement the Test Utility Methods.

使用测试驱动开发编写测试实用程序方法

Using Test-Driven Development to Write Test Utility Methods

一旦我们完成了使用测试实用程序方法的测试方法的编写,我们就可以开始编写测试实用程序方法本身的过程。在此过程中,我们可以通过编写测试实用程序测试来利用测试驱动开发(请参阅测试实用程序方法)。编写这些验证测试实用程序方法行为的单元测试并不需要很长时间,我们将对它们更有信心。

Once we are finished writing the Test Method(s) that used the Test Utility Method, we can start the process of writing the Test Utility Method itself. Along the way, we can take advantage of test-driven development by writing Test Utility Tests (see Test Utility Method). It doesn't take very long to write these unit tests that verify the behavior of our Test Utility Methods and we will have much more confidence in them.

我们从简单情况开始(例如,断言两个包含相同项目的相同集合相等),然后逐步处理测试方法 实际需要的最复杂情况(例如,两个包含相同项目但顺序不同的集合)。 TDD 帮助我们找到测试实用程序方法的最小实现,它可能比完整的通用解决方案简单得多。编写处理实际上不需要的情况的通用逻辑是没有意义的,但在自定义断言中包含一个或两个Guard 断言以在其不支持的情况下使测试失败可能是值得的。

We start with the simple case (say, asserting the equality of two identical collections that hold the same item) and work up to the most complicated case that the Test Methods actually require (say, two collections that contain the same two items but in different order). TDD helps us find the minimal implementation of the Test Utility Method, which may be much simpler than a complete generic solution. There is no point in writing generic logic that handles cases that aren't actually needed but it may be worthwhile to include a Guard Assertion or two inside the Custom Assertion to fail tests in cases it doesn't support.

可重复使用的验证逻辑放在哪里?

Where to Put Reusable Verification Logic?

假设我们决定使用提取方法重构来创建一些可重用的自定义断言,或者我们决定使用验证方法以揭示意图的方式编写测试。我们应该把这些可重用的测试逻辑放在哪里?最明显的地方是在测试用例类(第 373页) 本身中。我们可以通过使用上拉方法 [Fowler] 重构将它们上移到测试用例超类(638页) 或使用移动方法 [Fowler] 重构将它们移动到测试助手(第643页) ,从而允许更广泛地重用这些逻辑。这个问题将在第 12 章“组织我们的测试”中详细讨论。

Suppose we have decided to use Extract Method refactorings to create some reusable Custom Assertions or we have decided to write our tests in an intent-revealing way using Verification Methods. Where should we put these bits of reusable test logic? The most obvious place is in the Testcase Class (page 373) itself. We can allow this logic to be reused more broadly by using a Pull-Up Method [Fowler] refactoring to move them up to a Testcase Superclass (page 638) or a Move Method [Fowler] refactoring to move them into a Test Helper (page 643). This issue is discussed in more detail in Chapter 12, Organizing Our Tests.

下一步是什么?

What's Next?

以上关于验证预期结果的技术的讨论结束了我们对使用 xUnit 进行自动化测试的基本技术的介绍。第 11 章使用测试替身”介绍了一些涉及使用测试替身的高级技术。

This discussion of techniques for verifying the expected outcome concludes our introduction to the basic techniques of automating tests using xUnit. Chapter 11, Using Test Doubles, introduces some advanced techniques involving the use of Test Doubles.

第 11 章

使用测试替身

Chapter 11

Using Test Doubles

 

关于本章

About This Chapter

最后几章以第 10 章“结果验证”为结尾,介绍了使用 xUnit 系列测试自动化框架(第298页)运行测试的基本机制。在大多数情况下,我们假设 SUT 的设计使得它可以轻松地独立于其他软件进行测试。当一个类不依赖于任何其他类时,测试它相对简单,本章中描述的技术是不必要的。当一个类依赖于其他类时,我们有两种选择:我们可以将它与它所依赖的所有其他类一起测试,或者我们可以尝试将它与其他类隔离,以便我们可以单独测试它。本章介绍将 SUT 与它所依赖的其他软件组件隔离的技术。

The last few chapters concluding with Chapter 10, Result Verification, introduced the basic mechanisms of running tests using the xUnit family of Test Automation Frameworks (page 298). For the most part we assumed that the SUT was designed such that it could be tested easily in isolation of other pieces of software. When a class does not depend on any other classes, testing it is relatively straightforward and the techniques described in this chapter are unnecessary. When a class does depend on other classes, we have two choices: We can test it together with all the other classes it depends on or we can try to isolate it from the other classes so that we can test it by itself. This chapter introduces techniques for isolating the SUT from the other software components on which it depends.

什么是间接输入和输出?

What Are Indirect Inputs and Outputs?

以组或集群形式测试类的问题在于,很难覆盖代码中的所有路径。依赖组件 (DOC) 可能会返回值或抛出影响 SUT 行为的异常,但可能很难或不可能导致某些情况发生。从 DOC 收到的间接输入可能无法预测(例如系统时钟或日历)。在其他情况下,DOC 可能在测试环境中不可用,甚至可能不存在。在这些情况下,我们如何测试依赖类?

The problem with testing classes in groups or clusters is that it becomes very hard to cover all the paths through the code. The depended-on component (DOC) may return values or throw exceptions that affect the behavior of the SUT, but it may prove difficult or impossible to cause certain cases to occur. The indirect inputs received from the DOC may be unpredictable (such as the system clock or calendar). In other cases, the DOC may not be available in the test environment or may not even exist. How can we test dependent classes in these circumstances?

在其他情况下,我们需要验证执行 SUT 的某些副作用确实发生了。如果监控 SUT 的这些间接输出太困难(或者检索它们的成本太高),我们的自动化测试的有效性可能会受到影响。

In other cases, we need to verify that certain side effects of executing the SUT have, indeed, occurred. If it is too difficult to monitor these indirect outputs of the SUT (or if it is too expensive to retrieve them), the effectiveness of our automated testing may be compromised.

从本章标题中您肯定已经猜到了,解决这些问题的方法通常是使用测试替身第 522页)。我们将首先介绍如何使用测试替身来测试间接输入和输出。然后,我们将介绍这些有用机制的其他一些用途。

As you will no doubt have guessed from the title of this chapter, the solution to these problems is often the use of a Test Double (page 522). We will start by looking at how we can use Test Doubles to test indirect inputs and outputs. We will then describe a few other uses of these helpful mechanisms.

为什么我们关心间接投入?

Why Do We Care about Indirect Inputs?

对 DOC 的调用通常会返回对象或值、更新其参数甚至抛出异常。SUT 中的许多执行路径旨在处理这些返回值并处理可能的异常。如果不测试这些路径,则会导致未经测试的代码(请参阅第268页的生产错误)。这些路径可能是最难有效测试的路径,但如果在生产中首次执行,它们也是最有可能导致灾难性故障的路径之一。

Calls to DOCs often return objects or values, update their arguments or even throw exceptions. Many of the execution paths within the SUT are intended to deal with these return values and to handle the possible exceptions. Leaving these paths untested leads to Untested Code (see Production Bugs on page 268). These paths can be the most challenging to test effectively but are also among the most likely to lead to catastrophic failures if exercised for the very first time in production.

我们当然不希望在生产中第一次执行异常处理代码。如果代码编码不正确怎么办?显然,对此类代码进行自动化测试是非常理想的。测试挑战是以某种方式使 DOC 抛出异常,以便可以测试错误路径。我们预计 DOC 会抛出的异常是间接输入测试条件的一个很好的例子(图 11.1)。我们注入此输入的方法是控制点

We certainly would rather not have the exception-handling code execute for the first time in production. What if it was coded incorrectly? Clearly, it would be highly desirable to have automated tests for such code. The testing challenge is to somehow cause the DOC to throw an exception so that the error path can be tested. The exception we expect the DOC to throw is a good example of an indirect input test condition (Figure 11.1). Our means of injecting this input is a control point.

图 11.1。SUT 从 DOC 接收的间接输入。SUT 的输入并非全部来自测试。一些间接输入以返回值、更新的参数或抛出的异常的形式来自 SUT 调用的其他组件。

Figure 11.1. An indirect input being received by the SUT from a DOC. Not all inputs of the SUT come from the test. Some indirect inputs come from other components called by the SUT in the form of return values, updated parameters, or exceptions thrown.

图像

为什么我们关心间接产出?

Why Do We Care about Indirect Outputs?

封装的概念常常指导我们不要关心某些东西是如何实现的。毕竟,这就是封装的全部目的——减轻我们接口的客户端关心我们实现的需要。在测试时,我们会尝试精确地验证实现,这样我们的客户就不必关心它了。

The concept of encapsulation often directs us to not care about how something is implemented. After all, that is the whole purpose of encapsulation—to alleviate the need for clients of our interface to care about our implementation. When testing, we try to verify the implementation precisely so our clients do not have to care about it.

假设一个组件的 API 中有一个方法没有返回任何内容,或者至少没有返回任何可用于确定其是否正确执行其功能的内容。在这种情况下,我们别无选择,只能通过后门进行测试。消息日志系统就是一个很好的例子。对记录器 API 的调用很少会返回任何表明其正确执行其工作的内容。确定消息日志系统是否按预期运行的唯一方法是通过其他接口与其交互,该接口允许我们检索已记录的消息。

Consider for a moment a component that has a method in its API that returns nothing—or at least nothing that can be used to determine whether it has performed its function correctly. In this situation, we have no choice but to test through the back door. A good example of this is a message logging system. Calls to the API of a logger rarely return anything that indicates it did its job correctly. The only way to determine whether the message logging system is working as expected is to interact with it through some other interface—one that allows us to retrieve the logged messages.

记录器的客户端可以指定在满足某些条件时调用记录器。这些调用在客户端界面上不可见,但通常是客户端需要满足的要求,因此,也是我们想要测试的内容。应该导致消息被记录的情况是间接输出测试条件(图 11.2),我们需要为其编写测试,以避免出现未经测试的需求(参见生产错误)。我们查看此输出的方式是观察点

A client of the logger may specify that the logger be called when certain conditions are met. These calls will not be visible on the client's interface but would typically be a requirement that the client needs to satisfy and, therefore, would be something we want to test. The circumstances that should result in a messaging being logged are indirect output test conditions (Figure 11.2) for which we need to write tests so that we can avoid having Untested Requirements (see Production Bugs). Our means of seeing this output is an observation point.

图 11.2。SUT 接收的间接输出。并非所有 SUT 输出都可直接供测试使用。有些间接输出以方法调用或消息的形式发送到其他组件。

Figure 11.2. An indirect output being received by the SUT. Not all outputs of the SUT are directly visible to the test. Some indirect outputs are sent to other components in the form of method calls or messages.

图像

在其他情况下,SUT 确实会产生可见的行为,可以通过前门进行验证,但也会产生一些预期的副作用。两种输出都需要在我们的测试中进行验证。有时,这种测试只是将间接输出的断言添加到现有测试中以验证​​未测试需求

In other cases, the SUT does produce visible behavior that can be verified through the front door but also has some expected side effects. Both outputs need to be verified in our tests. Sometimes this testing is simply a matter of adding assertions for the indirect outputs to the existing tests to verify the Untested Requirement.

我们如何控制间接投入?

How Do We Control Indirect Inputs?

使用间接输入进行测试比使用间接输出进行测试要简单一些,因为用于测试输出的技术建立在用于测试输入的技术之上。让我们首先深入研究间接输入。

Testing with indirect inputs is a bit simpler than testing with indirect outputs because the techniques used to test outputs build on those used to test inputs. Let's delve into indirect inputs first.

要使用间接输入测试 SUT ,我们必须能够很好地控制 DOC,使其返回所有可能的返回值。这意味着需要有合适的控制点。

To test the SUT with indirect inputs, we must be able to control the DOC well enough to cause it to return every possible kind of return value. That implies the availability of a suitable control point.

我们希望能够通过此控制点诱导的间接输入类型包括

Examples of the kinds of indirect inputs we want to be able to induce via this control point include

  • 方法/函数的返回值
  • Return values of methods/functions
  • 可更新参数的值
  • Values of updatable arguments
  • 可能引发的异常
  • Exceptions that could be thrown

通常,测试可以与 DOC 交互以设置其如何响应请求。例如,如果组件提供对数据库中数据的访问,那么我们可以使用后门设置(请参阅第327页的后门操作)将特定值插入数据库,使组件以所需的方式响应(例如,未找到任何项目、找到一个项目、找到许多项目)。(参见图 11.3。)在这种特定情况下,我们可以使用数据库本身作为控制点。

Often, the test can interact with the DOC to set up how it will respond to requests. For example, if a component provides access to data in a database, then we can use Back Door Setup (see Back Door Manipulation on page 327) to insert specific values into a database that cause the component to respond in the desired ways (e.g., no items found, one item found, many items found). (See Figure 11.3.) In this specific case, we can use the database itself as a control point.

图 11.3. 使用后门操纵间接控制和观察 SUT。当 SUT 将其状态存储在另一个组件中时,我们可以通过让测试通过“后门”直接与其他组件交互来操纵该状态。

Figure 11.3. Using Back Door Manipulation to indirectly control and observe the SUT. When the SUT stores its state in another component, we may be able to manipulate that state by having the test interact directly with the other component via a "back door."

图像

但在大多数情况下,这种方法既不实用,也是不可能的。我们可能无法使用真实组件,原因如下:

In most cases, however, this approach is neither practical nor even possible. We might not be able to use the real component for the following reasons:

  • 无法操纵实际组件来产生所需的间接输入。只有实际组件中真正的软件错误才会产生对 SUT 的所需输入。
  • The real component cannot be manipulated to produce the desired indirect input. Only a true software error within the real component would result in the desired input to the SUT.
  • 可以操纵真实组件来实现输入,但这样做并不划算。
  • The real component could be manipulated to make the input occur but doing so would not be cost-effective.
  • 可以操纵真实组件以使输入发生,但这样做可能会产生不可接受的副作用。
  • The real component could be manipulated to make the input occur but doing so could have unacceptable side effects.
  • 真正的组件尚未可供使用。
  • The real component is not yet available for use.

如果我们不能使用真实组件作为控制点,那么我们必须用一个我们可以控制的组件来替换它。这种替换可以通过多种不同的方式完成,这些方式是本章后面“安装测试替身”一节的重点。最常见的方法是配置一个测试桩第 529页),并为其函数返回一组值,然后将此测试桩安装到 SUT 中。在 SUT 执行期间,测试桩接收调用并返回先前配置的响应(图 11.4)。它已成为我们的控制点。

If we cannot use the real component as a control point, then we have to replace it with one that we can control. This replacement can be done in a number of different ways, which are the focus of the section Installing the Test Double later in this chapter. The most common approach is to configure a Test Stub (page 529) with a set of values to return from its functions and then to install this Test Stub into the SUT. During execution of the SUT, the Test Stub receives the calls and returns the previously configured responses (Figure 11.4). It has become our control point.

图 11.4. 使用测试桩作为间接输入的控制点。使用控制点将间接输入注入 SUT 的一种方法是安装测试桩来代替 DOC。在执行 SUT 之前,我们告诉测试桩在调用时应该返回给 SUT 什么。此策略允许我们强制 SUT 执行其所有代码路径。

Figure 11.4. Using a Test Stub as a control point for indirect inputs. One way to use a control point to inject indirect inputs into the SUT is to install a Test Stub in place of the DOC. Before exercising the SUT, we tell the Test Stub what it should return to the SUT when it is called. This strategy allows us to force the SUT through all its code paths.

图像

我们如何验证间接输出?

How Do We Verify Indirect Outputs?

在正常使用中,当 SUT 运行时,它会自然地与其所依赖的组件进行交互。要测试间接输出,我们必须能够观察 SUT 对 DOC 的 API 的调用(图 11.5)。此外,如果我们需要测试超越这一点,我们需要能够控制返回的值(如在间接输入的讨论中所述)。

In normal usage, as the SUT is exercised, it interacts naturally with the component(s) upon which it depends. To test the indirect outputs, we must be able to observe the calls that the SUT makes to the API of the DOC (Figure 11.5). Furthermore, if we need the test to progress beyond that point, we need to be able to control the values returned (as was discussed in the discussion of indirect inputs).

图 11.5. 使用行为验证来验证 SUT 的间接输出。当我们关心 SUT 对其他组件的确切调用时,我们可能必须进行行为验证,而不是简单地验证 SUT 的测试后状态。

Figure 11.5. Using Behavior Verification to verify the indirect outputs of the SUT. When we care about exactly what calls our SUT makes to other components, we may have to do Behavior Verification rather than simply verifying the post-test state of the SUT.

图像

在许多情况下,测试可以使用 DOC 作为观察点来了解其使用方式。例如:

In many cases, the test can use the DOC as an observation point to find out how it has been used. For example:

  • 我们可以向文件系统询问 SUT 写入的文件的内容,以验证它是否存在以及是否写入了预期的内容。
  • We can ask the file system for the contents of a file that the SUT has written to verify that it exists and was written with the expected contents.
  • 我们可以向数据库询问表或特定记录的内容,以验证 SUT 是否将预期的记录写入数据库。
  • We can ask the database for the contents of a table or specific record to verify that the SUT wrote the expected records to the database.
  • 我们可以直接与电子邮件发送组件交互,询问 SUT 是否要求其发送特定的电子邮件。
  • We can interact directly with the e-mail sending component to ask whether the SUT had asked it to send a particular e-mail.

这些都是后门验证的示例(请参阅第 327页的后门操纵)。一些 DOC 允许我们以某种方式配置它们的行为,以便我们可以使用它们让测试人员了解它们的使用方式:

These are all examples of Back Door Verification (see Back Door Manipulation on page 327). Some DOCs allow us to configure their behavior in such a way that we can use them to keep the test informed of how they are being used:

  • 我们可以要求文件系统在文件创建或修改时通知测试,以便我们可以验证其内容。
  • We can ask the file system to notify the test whenever a file is created or modified so we can verify its contents.
  • 我们可以使用数据库触发器在写入或删除记录时通知测试。
  • We can use a database trigger to notify the test when a record is written or deleted.
  • 我们可以配置电子邮件发送组件,将所有发出的电子邮件发送到测试。
  • We can configure the e-mail sending component to deliver all outgoing e-mail to the test.

有时,正如我们在间接输入中看到的那样,使用真实组件作为观察点是不切实际的。当所有其他方法都失败时,我们可能需要用测试特定的替代方案替换真实组件。例如,我们可能需要出于以下原因这样做:

Sometimes, as we have seen with indirect inputs, it is not practical to use the real component as an observation point. When all else fails, we may need to replace the real component with a test-specific alternative. For example, we might need to do this for the following reasons:

  • 无法查询对 DOC 的调用(或内部状态)。
  • The calls to (or the internal state of) the DOC cannot be queried.
  • 可以查询真实组件,但这样做的成本太高。
  • The real component can be queried but doing so is cost-prohibitive.
  • 可以查询真实组件但这样做会产生不可接受的副作用。
  • The real component can be queried but doing so has unacceptable side effects.
  • 真正的组件尚未可供使用。
  • The real component is not yet available for use.

真实组件的替换可以通过多种不同的方式进行,我们将在安装测试替身中讨论这一点

The replacement of the real component can be done in a number of different ways, as will be discussed in Installing the Test Double.

间接输出验证有两种基本样式。程序行为验证(参见行为验证)在 SUT 执行期间捕获对 DOC 的调用(或其结果),然后在 SUT 完成执行后将它们与预期调用进行比较。此验证涉及用测试间谍(第 538页)替换可替代依赖项。在 SUT 执行期间,测试间谍接收调用并记录它们。在测试方法第 348页)完成 SUT 的执行后,它从测试间谍中检索实际调用并使用断言方法(第362页)将它们与预期调用进行比较(图 11.6

Two basic styles of indirect output verification are available. Procedural Behavior Verification (see Behavior Verification) captures the calls to a DOC (or their results) during SUT execution and then compares them with the expected calls after the SUT has finished executing. This verification involves replacing a substitutable dependency with a Test Spy (page 538). During execution of the SUT, the Test Spy receives the calls and records them. After the Test Method (page 348) has finished exercising the SUT, it retrieves the actual calls from the Test Spy and uses Assertion Methods (page 362) to compare them with the expected calls (Figure 11.6).

图 11.6. 使用测试间谍作为 SUT 间接输出的观察点。 实现行为验证的一种方法是安装测试间谍来代替间接输出的目标。 在执行 SUT 后,测试会向测试间谍询问有关如何使用它的信息,并使用断言将该信息与预期行为进行比较。

Figure 11.6. Using a Test Spy as an observation point for indirect outputs of the SUT. One way to implement Behavior Verification is to install a Test Spy in place of the target of the indirect outputs. After exercising the SUT, the test asks the Test Spy for information about how it was used and compares that information to the expected behavior using assertions.

图像

预期行为参见行为验证涉及在测试的装置设置阶段构建“行为规范”,然后将实际行为与此预期行为进行比较。通常通过加载带有一组预期过程调用描述的模拟对象第 544页)并将此对象安装到 SUT 中来完成(图 11.7)。在执行 SUT 期间,模拟对象接收调用并将它们与先前定义的预期调用(“行为规范”)进行比较。随着测试的进行,如果模拟对象收到意外调用,则测试立即失败。测试失败回溯将显示 SUT 中发生问题的确切位置,因为断言方法是从模拟对象调用的而模拟对象又由 SUT 调用。我们还可以准确地看到测试方法中SUT 被执行的位置。

Expected Behavior (see Behavior Verification) involves building a "behavior specification" during the fixture setup phase of the test and then comparing the actual behavior with this Expected Behavior. It is typically done by loading a Mock Object (page 544) with a set of expected procedure call descriptions and installing this object into the SUT (Figure 11.7). During execution of the SUT, the Mock Object receives the calls and compares them to the previously defined expected calls (the "behavior specification"). As the test proceeds, if the Mock Object receives an unexpected call, it fails the test immediately. The test failure traceback will show the exact location in the SUT where the problem occurred because the Assertion Methods are called from the Mock Object, which is in turn called by the SUT. We can also see exactly where in the Test Method the SUT was being exercised.

图 11.7. 使用模拟对象作为 SUT 间接输出的观察点。实现行为验证的另一种方法是安装模拟对象来代替间接输出的目标。当 SUT 调用 DOC 时,模拟对象使用断言将实际调用和参数与预期调用和参数进行比较。

Figure 11.7. Using a Mock Object as an observation point for indirect outputs of the SUT. Another way to implement Behavior Verification is to install a Mock Object in place of the target of the indirect outputs. As the SUT makes calls to the DOC, the Mock Object uses assertions to compare the actual calls and arguments with the expected calls and arguments.

图像

当我们使用测试间谍模拟对象时,我们可能还必须将其用作 SUT 所依赖的任何间接输入的控制点,在调用测试间谍模拟对象之后,以允许测试执行继续。

When we use a Test Spy or a Mock Object, we may also have to employ it as a control point for any indirect inputs on which the SUT depends after the Test Spy or Mock Object has been called to allow test execution to continue.

使用 Double 进行测试

Testing with Doubles

现在您可能想知道如何用某种东西来替换那些不灵活、不合作的真实组件,以便更容易控制间接输入和验证间接输出。

By now you are probably wondering about how to replace those inflexible and uncooperative real components with something that makes it easier to control the indirect inputs and to verify the indirect outputs.

正如我们所见,要测试间接输入,我们必须能够很好地控制 DOC,使其返回所有可能的返回值(有效、无效和异常)。要测试间接输出,我们必须能够跟踪 SUT 对其他组件的调用。测试替身是一种更具协作性的对象,它让我们可以按照自己想要的方式编写测试。

As we have seen, to test the indirect inputs, we must be able to control the DOC well enough to cause it to return every possible kind of return value (valid, invalid, and exception). To test indirect outputs, we must be able to track the calls the SUT makes to other components. A Test Double is a type of object that is much more cooperative and lets us write tests the way we want to.

测试替身的类型

Types of Test Doubles

测试替身是我们为了运行测试而安装的任何对象或组件,用于替代真实组件。根据我们使用替身的原因,测试替身可以有以下四种行为方式(总结在图 11.8中):

A Test Double is any object or component that we install in place of the real component for the express purpose of running a test. Depending on the reason why we are using it, a Test Double can behave in one of four ways (summarized in Figure 11.8):

  • 虚拟对象(第728页)是一个占位符对象,它作为参数(或参数的属性)传递给 SUT,但从未实际使用过。
  • A Dummy Object (page 728) is a placeholder object that is passed to the SUT as an argument (or an attribute of an argument) but is never actually used.
  • 测试是一个对象,用于替换 SUT 所依赖的实际组件,以便测试可以控制 SUT 的间接输入。它允许测试强制 SUT 执行它原本可能不会执行的路径。测试间谍是测试桩的更强大版本,它可用于验证 SUT 的间接输出,方法是让测试在执行 SUT 后检查它们。
  • A Test Stub is an object that replaces a real component on which the SUT depends so that the test can control the indirect inputs of the SUT. It allows the test to force the SUT down paths it might not otherwise exercise. A Test Spy, which is a more capable version of a Test Stub, can be used to verify the indirect outputs of the SUT by giving the test a way to inspect them after exercising the SUT.
  • 模拟对象是一个替换 SUT 所依赖的真实组件的对象,以便测试可以验证其间接输出。
  • A Mock Object is an object that replaces a real component on which the SUT depends so that the test can verify its indirect outputs.
  • 伪对象(第551页)(或简称为“伪”)是用相同功能的替代实现取代真实 DOC 功能的对象。
  • A Fake Object (page 551) (or just "Fake" for short) is an object that replaces the functionality of the real DOC with an alternative implementation of the same functionality.

图 11.8。 存在几种类型的测试替身。虚拟对象实际上是值模式的替代品。测试桩用于验证间接输入;测试间谍和模拟对象用于验证间接输出。伪对象模拟实际依赖组件的行为,但具有测试友好的特性。

Figure 11.8. Several kinds of Test Doubles exist. Dummy Objects are really an alternative to the value patterns. Test Stubs are used to verify indirect inputs; Test Spies and Mock Objects are used to verify indirect outputs. Fake objects emulate the behavior of the real depended-on component, but with test-friendly characteristics.

图像

虚拟对象

虚拟对象是测试替身的退化形式。它们存在的唯一目的是在方法之间传递;它们从不被使用。也就是说,虚拟对象除了存在之外不做任何事情。通常,我们可以不使用“null”(或“nil”或“nothing”);在其他时候,我们可能被迫创建一个真实对象,因为代码需要非空值。在动态类型语言中,几乎任何真实对象都可以;在静态类型语言中,我们必须确保虚拟对象与它作为参数传递或分配给它的变量“类型兼容”。

Dummy Objects are a degenerate form of Test Double. They exist solely so that they can be passed around from method to method; they are never used. That is, Dummy Objects are not expected to do anything except exist. Often, we can get away with using "null" (or "nil" or "nothing"); at other times, we may be forced to create a real object because the code expects something non-null. In dynamically typed languages, almost any real object will do; in statically typed languages, we must make sure that the Dummy Object is "type-compatible" with the parameter it is being passed as or the variable to which it is being assigned.

在以下示例中,我们将的实例传递DummyCustomerInvoice构造函数以满足强制参数。我们不希望DummyCustomer在此处测试的代码使用。

In the following example, we pass an instance of DummyCustomer to the Invoice constructor to satisfy a mandatory argument. We do not expect the DummyCustomer to be used by the code we are testing here.

public void testInvoice_addLineItem_DO() {

      final int QUANTITY = 1;

      Product product = new Product("虚拟产品名称",

                                                            getUniqueNumber());

      Invoice inv = new Invoice( new DummyCustomer() );

     LineItem expItem = new LineItem(inv, product, QUANTITY);

      // 练习

      inv.addItemQuantity(product, QUANTITY);

      // 验证

      列表 lineItems = inv.getLineItems();

     assertEquals("项目数量", lineItems.size(), 1);

     LineItem actual = (LineItem)lineItems.get(0);

      assertLineItemsEqual("", expItem, actual);

}

public  void  testInvoice_addLineItem_DO()  {

      final  int  QUANTITY  =  1;

      Product  product  =  new  Product("Dummy  Product  Name",

                                                            getUniqueNumber());

      Invoice  inv  =  new  Invoice(  new  DummyCustomer()  );

     LineItem  expItem  =  new  LineItem(inv,  product,  QUANTITY);

      //  Exercise

      inv.addItemQuantity(product,  QUANTITY);

      //  Verify

      List  lineItems  =  inv.getLineItems();

     assertEquals("number  of  items",  lineItems.size(),  1);

     LineItem  actual  =  (LineItem)lineItems.get(0);

      assertLineItemsEqual("",  expItem,  actual);

}

 

请注意,虚拟对象与空对象[PLOPD3]不同。虚拟对象SUT 使用,因此其行为无关紧要。相比之下,空对象被 SUT 使用,但被设计为不执行任何操作。这是一个很小但非常重要的区别!

Note that a Dummy Object is not the same as a Null Object [PLOPD3]. A Dummy Object is not used by the SUT, so its behavior is irrelevant. By contrast, a Null Object is used by the SUT but is designed to do nothing. That's a small but very important distinction!

虚拟对象与其他测试替身属于不同的类别;它们实际上是属性值模式(例如文字值第 714)、生成值(第723)和派生值第 718))的替代品。因此,我们不需要“配置”或“安装”它们。事实上,我们对其他测试替身所说的内容几乎都不适用于虚拟对象,因此我们不会在本章中再次提及它们。

Dummy Objects are in a different league than the other Test Doubles; they are really an alternative to the attribute value patterns such as Literal Value (page 714), Generated Value (page 723), and Derived Value (page 718). Therefore, we don't need to "configure" them or "install" them. In fact, almost nothing we say about the other Test Doubles applies to Dummy Objects, so we won't mention them again in this chapter.

测试桩

测试是一个对象,它在调用测试桩的方法时充当控制点,向 SUT 提供间接输入。它的使用使我们能够在 SUT 中执行未经测试的代码路径,否则这些路径在测试期间可能无法遍历。响应器(请参阅测试桩)是一个基本的测试桩,用于通过方法调用的正常返回向 SUT 注入有效和无效的间接输入。破坏者(请参阅测试桩)是一个特殊的测试桩,它引发异常或错误以向 SUT 注入异常的间接输入。因为过程编程语言不支持对象,所以它们迫使我们使用过程测试桩(请参阅测试桩)。

A Test Stub is an object that acts as a control point to deliver indirect inputs to the SUT when the Test Stub's methods are called. Its use allows us to exercise Untested Code paths in the SUT that might otherwise be impossible to traverse during testing. A Responder (see Test Stub) is a basic Test Stub that is used to inject valid and invalid indirect inputs into the SUT via normal returns from method calls. A Saboteur (see Test Stub) is a special Test Stub that raises exceptions or errors to inject abnormal indirect inputs into the SUT. Because procedural programming languages do not support objects, they force us to use Procedural Test Stubs (see Test Stub).

在以下示例中,Saboteur(在 Java 中作为匿名内部类实现)在 SUT 调用方法时抛出异常,getTime以便我们验证 SUT 在这种情况下是否行为正确:

In the following example, the Saboteur—implemented as an anonymous inner class in Java—throws an exception when the SUT calls the getTime method to allow us to verify that the SUT behaves correctly in this case:

public void testDisplayCurrentTime_exception()

          throws Exception {

      // Fixture 设置

      // 定义并实例化测试桩

      TimeProvider testStub = new TimeProvider()

            { // 匿名内部测试桩

                public Calendar getTime() throws TimeProviderEx {

                      throw new TimeProviderEx("Sample");

            }

      };

      // 实例化 SUT

      TimeDisplay sut = new TimeDisplay();

      sut.setTimeProvider(testStub);

      // 练习 SUT

      String result = sut.getCurrentTimeAsHtmlFragment();

      // 验证直接输出

      String expectedTimeString =

               "<span class=\"error\">Invalid Time</span>";

     assertEquals("Exception", expectedTimeString, result);

}

public  void  testDisplayCurrentTime_exception()

          throws  Exception  {

      //  Fixture  setup

      //      Define  and  instantiate  Test  Stub

      TimeProvider  testStub  =  new  TimeProvider()

            {  //  Anonymous  inner  Test  Stub

                public  Calendar  getTime()  throws  TimeProviderEx  {

                      throw  new  TimeProviderEx("Sample");

            }

      };

      //      Instantiate  SUT

      TimeDisplay  sut  =  new  TimeDisplay();

      sut.setTimeProvider(testStub);

      //  Exercise  SUT

      String  result  =  sut.getCurrentTimeAsHtmlFragment();

      //  Verify  direct  output

      String  expectedTimeString  =

               "<span  class=\"error\">Invalid  Time</span>";

     assertEquals("Exception",  expectedTimeString,  result);

}

 

在过程编程语言中,过程测试桩要么是 (1)作为尚未编写的过程的替代实现的测试桩,要么是 (2) 链接到程序中的过程的替代实现,而不是过程的实际实现。传统上,引入过程测试桩是为了允许在等待其他代码准备就绪时进行调试。它们很少在运行时“交换”——这在大多数过程语言中很难做到。如果我们不介意在生产代码中引入测试逻辑(第 217页),我们可以使用测试钩子(第 709页)(如在 SUT 中)实现过程测试桩。以下清单说明了这一点:if  testing  then  ...  else

In procedural programming languages, a Procedural Test Stub is either (1) a Test Stub implemented as a stand-in for an as-yet-unwritten procedure or (2) an alternative implementation of a procedure linked into the program instead of the real implementation of the procedure. Traditionally, Procedural Test Stubs are introduced to allow debugging to proceed while we are waiting for other code to be ready. They are rarely "swapped in" at runtime—this is hard to do in most procedural languages. If we do not mind introducing Test Logic in Production (page 217) code, we can implement a Procedural Test Stub using Test Hooks (page 709) such as if  testing  then  ...  else in the SUT. This is illustrated in the following listing:

public Calendar getTime() throws TimeProviderEx {

      Calendar theTime = new GregorianCalendar();

      if (TESTING) {

            theTime.set(Calendar.HOUR_OF_DAY, 0);

            theTime.set(Calendar.MINUTE, 0);}

      else {

           // 只返回日历

      }

      return theTime;

};

public  Calendar  getTime()  throws  TimeProviderEx  {

      Calendar  theTime  =  new  GregorianCalendar();

      if  (TESTING)  {

            theTime.set(Calendar.HOUR_OF_DAY,  0);

            theTime.set(Calendar.MINUTE,  0);}

      else  {

           //  just  return  the  calendar

      }

      return  theTime;

};

 

关键的例外发生在支持过程变量的语言中。1只要客户端代码通过过程变量访问要替换的过程,这些变量就允许我们实现动态绑定

The key exception occurs in languages that support procedure variables.1 These variables allow us to implement dynamic binding as long as the client code accesses the procedure to be replaced via a procedure variable.

测试间谍

测试间谍是一个可以充当 SUT 间接输出的观察点的对象。除了测试桩的功能外,它还增加了悄悄记录 SUT 对其方法的所有调用的能力。测试的验证部分通过使用一系列断言将测试间谍收到的实际调用与预期调用进行比较,对这些调用执行程序行为验证。

A Test Spy is an object that can act as an observation point for the indirect outputs of the SUT. To the capabilities of a Test Stub, it adds the ability to quietly record all calls made to its methods by the SUT. The verification part of the test performs Procedural Behavior Verification on those calls by using a series of assertions to compare the actual calls received by the Test Spy with the expected calls.

下面的示例使用Test Spy上的检索接口(参见Test Spy)来验证SUT在调用方法时是否将正确的信息作为参数传递(的方法)。logMessageremoveFlightfacade

The following example uses the Retrieval Interface (see Test Spy) on the Test Spy to verify that the correct information was passed as arguments in the call to the logMessage method by the SUT (the removeFlight method of the facade).

public void testRemoveFlightLogging_recordingTestStub()

              throws Exception {

      // Fixture 设置

      FlightDto expectedFlightDto = createAnUnregFlight();

      FlightManagementFacade Facade =

                new FlightManagementFacadeImpl();

      // 测试替身设置

      AuditLogSpy logSpy = new AuditLogSpy();

      Facade.setAuditLog(logSpy);

      // 练习

      Facade.removeFlight(expectedFlightDto.getFlightNumber());

      // 验证状态

      assertFalse("flight 在被移除后仍然存在",

                        Facade.flightExists( expectedFlightDto.

                                                                 getFlightNumber()));

      // 使用间谍的检索接口验证间接输出

     assertEquals("呼叫次数", 1,

                          logSpy.getNumberOfCalls());

     assertEquals("操作代码",

                         Helper.REMOVE_FLIGHT_ACTION_CODE,

                         logSpy.getActionCode());

     断言Equals("日期", helper.getTodaysDateWithoutTime(),

                          logSpy.getDate());

     断言Equals("用户", Helper.TEST_USER_NAME,

                          logSpy.getUser());

     断言Equals("详细信息",

                          expectedFlightDto.getFlightNumber(),

                          logSpy.getDetail());

}

public  void  testRemoveFlightLogging_recordingTestStub()

              throws  Exception  {

      //  Fixture  setup

      FlightDto  expectedFlightDto  =  createAnUnregFlight();

      FlightManagementFacade  facade  =

                new  FlightManagementFacadeImpl();

      //        Test  Double  setup

      AuditLogSpy  logSpy  =  new  AuditLogSpy();

      facade.setAuditLog(logSpy);

      //  Exercise

      facade.removeFlight(expectedFlightDto.getFlightNumber());

      //  Verify  state

      assertFalse("flight  still  exists  after  being  removed",

                        facade.flightExists(  expectedFlightDto.

                                                                 getFlightNumber()));

      //  Verify  indirect  outputs  using  retrieval  interface  of  spy

     assertEquals("number  of  calls",  1,

                          logSpy.getNumberOfCalls());

     assertEquals("action  code",

                         Helper.REMOVE_FLIGHT_ACTION_CODE,

                         logSpy.getActionCode());

     assertEquals("date",  helper.getTodaysDateWithoutTime(),

                          logSpy.getDate());

     assertEquals("user",  Helper.TEST_USER_NAME,

                          logSpy.getUser());

     assertEquals("detail",

                          expectedFlightDto.getFlightNumber(),

                          logSpy.getDetail());

}

 
模拟对象

Mock 对象也是一个可以充当 SUT 间接输出观察点的对象。与测试桩一样,它可能需要返回信息以响应方法调用。此外,与测试间谍一样,Mock 对象会关注 SUT 是如何调用它的。但它与测试间谍不同,Mock 对象使用断言将收到的实际调用与先前定义的期望进行比较,并代表测试方法使测试失败。因此,我们可以重复使用用于在所有使用相同Mock 对象的测试中验证 SUT 间接输出的逻辑。Mock对象有两种基本类型:

A Mock Object is also an object that can act as an observation point for the indirect outputs of the SUT. Like a Test Stub, it may need to return information in response to method calls. Also like a Test Spy, a Mock Object pays attention to how it was called by the SUT. It differs from a Test Spy, however, in that the Mock Object compares actual calls received with the previously defined expectations using assertions and fails the test on behalf of the Test Method. As a consequence, we can reuse the logic employed to verify the indirect outputs of the SUT across all tests that use the same Mock Object. Mock Objects come in two basic flavors:

  • 如果正确的调用以与指定顺序不同的顺序接收,则严格的Mock 对象测试失败。
  • A strict Mock Object fails the test if the correct calls are received in a different order than was specified.
  • 宽松的2 Mock 对象可以容忍无序调用。有些宽松的Mock 对象可以容忍甚至忽略意外调用或未接调用。也就是说,Mock 对象可能只验证与预期调用相对应的实际调用。
  • A lenient2 Mock Object tolerates out-of-order calls. Some lenient Mock Objects tolerate or even ignore unexpected calls or missed calls. That is, the Mock Object may verify only those actual calls that correspond to expected ones.

以下测试使用预期调用的参数配置模拟对象logMessage。当 SUT(removeFlight方法)调用时logMessage模拟对象断言每个实际参数都等于预期参数。如果发现传递了任何错误的参数,则测试失败。

The following test configures a Mock Object with the arguments of the expected call to logMessage. When the SUT (the removeFlight method) calls logMessage, the Mock Object asserts that each of the actual arguments equals the expected argument. If it discovers that any wrong arguments were passed, the test fails.

public void testRemoveFlight_Mock() throws Exception {

      // Fixture 设置

      FlightDto expectedFlightDto = createAnonRegFlight();

      // Mock 配置

      ConfigurableMockAuditLog mockLog =

            new ConfigurableMockAuditLog();

      mockLog.setExpectedLogMessage(

                                              helper.getTodaysDateWithoutTime(),

                                              Helper.TEST_USER_NAME,

                                              Helper.REMOVE_FLIGHT_ACTION_CODE,

                                              expectedFlightDto.getFlightNumber());

      mockLog.setExpectedNumberCalls(1);

      // Mock 安装

      FlightManagementFacade Facade =

                new FlightManagementFacadeImpl();

      Facade.setAuditLog(mockLog);

      // 练习

      Facade.removeFlight(expectedFlightDto.getFlightNumber());

      // 验证

      assertFalse("flight 在被移除后仍然存在",

                        Facade.flightExists( expectedFlightDto.

                                                                getFlightNumber()));

      mockLog.verify();

}

public  void  testRemoveFlight_Mock()  throws  Exception  {

      //  Fixture  setup

      FlightDto  expectedFlightDto  =  createAnonRegFlight();

      //  Mock  configuration

      ConfigurableMockAuditLog  mockLog  =

            new  ConfigurableMockAuditLog();

      mockLog.setExpectedLogMessage(

                                              helper.getTodaysDateWithoutTime(),

                                              Helper.TEST_USER_NAME,

                                              Helper.REMOVE_FLIGHT_ACTION_CODE,

                                              expectedFlightDto.getFlightNumber());

      mockLog.setExpectedNumberCalls(1);

      //  Mock  installation

      FlightManagementFacade  facade  =

                new  FlightManagementFacadeImpl();

      facade.setAuditLog(mockLog);

      //  Exercise

      facade.removeFlight(expectedFlightDto.getFlightNumber());

      //  Verify

      assertFalse("flight  still  exists  after  being  removed",

                        facade.flightExists(  expectedFlightDto.

                                                                getFlightNumber()));

      mockLog.verify();

}

 

与测试桩 一样,模拟对象通常支持配置任何所需的间接输入,以允许 SUT 前进到生成正在验证的间接输出的点。

Like Test Stubs, Mock Objects often support configuration with any indirect inputs required to allow the SUT to advance to the point where it would generate the indirect outputs they are verifying.

假物体

对象与测试桩模拟对象完全不同,因为它既不受测试直接控制,也不受测试观察。伪对象用于在测试中替换真实 DOC 的功能,其原因并非验证间接输入和输出。通常,伪对象实现真实 DOC 的相同功能或功能子集,尽管方式要简单得多。使用伪对象的最常见原因是真实 DOC 尚未构建、速度太慢或在测试环境中不可用。

A Fake Object is quite different from a Test Stub or a Mock Object in that it is neither directly controlled nor observed by the test. The Fake Object is used to replace the functionality of the real DOC in a test for reasons other than verification of indirect inputs and outputs. Typically, a Fake Object implements the same functionality or a subset of the functionality of the real DOC, albeit in a much simpler way. The most common reasons for using a Fake Object are that the real DOC has not yet been built, is too slow, or is not available in the test environment.

侧栏“不使用共享装置,测试速度更快”(第 319页)描述了我的团队如何将所有数据库访问封装在持久层接口后面,然后将持久层组件替换为使用内存哈希表而不是真实数据库的组件,从而使我们的测试运行速度提高 50 倍。为此,我们使用了一个类似于以下的假数据库(请参阅假对象):

The sidebar "Faster Tests Without Shared Fixtures" (page 319) describes how my team encapsulated all database access behind a persistence layer interface and then replaced the persistence layer component with one that used in-memory hash tables instead of a real database, thereby making our tests run 50 times faster. To do so, we used a Fake Database (see Fake Object) that was something like this one:

公共类 InMemoryDatabase 实现 FlightDao{

      私有列表 airports = new Vector();

      公共 Airport createAirport(String airportCode,

                                        String name, String vicinityCity)

                     抛出 DataException, InvalidArgumentException {

            assertParamtersAreValid( airportCode, name, vicinityCity);

            assertAirportDoesntExist( airportCode);

            Airport result = new Airport(getNextAirportId(),

                      airportCode, name, createCity(nearbyCity));

            airports.add(result);

            返回结果;

      }

      公共 Airport getAirportByPrimaryKey(BigDecimal airportId)

                     抛出 DataException, InvalidArgumentException {

            assertAirportNotNull(airportId);

            Airport result = null;

            迭代器 i = airports.iterator();

            while (i.hasNext()) {

                  Airport airport = (Airport) i.next();

                  if (airport.getId().equals(airportId)) {

                        返回机场;

                  }

            }

            抛出新的DataException(“未找到机场:”+airportId);

      }

public  class  InMemoryDatabase  implements  FlightDao{

      private  List  airports  =  new  Vector();

      public  Airport  createAirport(String  airportCode,

                                        String  name,  String  nearbyCity)

                     throws  DataException,  InvalidArgumentException  {

            assertParamtersAreValid(    airportCode,  name,  nearbyCity);

            assertAirportDoesntExist(  airportCode);

            Airport  result  =  new  Airport(getNextAirportId(),

                      airportCode,  name,  createCity(nearbyCity));

            airports.add(result);

            return  result;

      }

      public  Airport  getAirportByPrimaryKey(BigDecimal  airportId)

                     throws  DataException,  InvalidArgumentException  {

            assertAirportNotNull(airportId);

            Airport  result  =  null;

            Iterator  i  =  airports.iterator();

            while  (i.hasNext())  {

                  Airport  airport  =  (Airport)  i.next();

                  if  (airport.getId().equals(airportId))  {

                        return  airport;

                  }

            }

            throw  new  DataException("Airport  not  found:"+airportId);

      }

 

提供测试替身

Providing the Test Double

提供测试替身有两种方法手工构建的测试替身(请参阅可配置测试替身由测试自动化程序编写)或动态生成的测试替身(请参阅可配置测试替身使用其他开发人员提供的框架或工具包在运行时生成)。3所有生成的测试替身本质上都必须是可配置测试替身;下一节将更详细地介绍这些组件。相比之下,手工构建的测试替身往往是硬编码测试替身(请参阅硬编码测试替身),但也可以经过一些额外努力使其可配置。以下代码示例演示了一个使用 Java匿名内部类构造的手工编码的内部测试替身(请参阅硬编码测试替身) :

There are two approaches to providing a Test Double: a Hand-Built Test Double (see Configurable Test Double on page 558), which is coded by the test automater, or a Dynamically Generated Test Double (see Configurable Test Double), which is generated at runtime using a framework or toolkit provided by some other developer.3 All generated Test Doubles must be, by their very nature, Configurable Test Doubles; these components are covered in more detail in the next section. Hand-Built Test Doubles, by contrast, tend to be Hard-Coded Test Doubles (page 568) but can also be made configurable with some additional effort. The following code sample illustrates a hand-coded Inner Test Double (see Hard-Coded Test Double) that uses Java's anonymous inner class construct:

public void testDisplayCurrentTime_AtMidnight_PS()

          throws Exception {

      // Fixture 设置

      // 定义并实例化测试桩

      TimeProvider testStub = new PseudoTimeProvider()

      { // 匿名内部桩

            public Calendar getTime(String timeZone) {

                  Calendar myTime = new GregorianCalendar();

                  myTime.set(Calendar.MINUTE, 0);

                  myTime.set(Calendar.HOUR_OF_DAY, 0);

                  return myTime;

            }

      };

      // 实例化 SUT

      TimeDisplay sut = new TimeDisplay();

      // 将测试桩注入 SUT

      sut.setTimeProvider(testStub);

      // 练习 SUT

      String result = sut.getCurrentTimeAsHtmlFragment();

      // 验证直接输出

      String expectedTimeString =

                 "<span class=\"tinyBoldText\">Midnight</span>";

     assertEquals("Midnight", expectedTimeString, result);

}

public  void  testDisplayCurrentTime_AtMidnight_PS()

          throws  Exception  {

      //  Fixture  setup

      //        Define  and  instantiate  Test  Stub

      TimeProvider  testStub  =  new  PseudoTimeProvider()

      {  //  Anonymous  inner  stub

            public  Calendar  getTime(String  timeZone)  {

                  Calendar  myTime  =  new  GregorianCalendar();

                  myTime.set(Calendar.MINUTE,  0);

                  myTime.set(Calendar.HOUR_OF_DAY,  0);

                  return  myTime;

            }

      };

      //      Instantiate  SUT

      TimeDisplay  sut  =  new  TimeDisplay();

      //      Inject  Test  Stub  into  SUT

      sut.setTimeProvider(testStub);

      //  Exercise  SUT

      String  result  =  sut.getCurrentTimeAsHtmlFragment();

      //  Verify  direct  output

      String  expectedTimeString  =

                 "<span  class=\"tinyBoldText\">Midnight</span>";

     assertEquals("Midnight",  expectedTimeString,  result);

}

 

通过提供一组称为伪对象的基类(请参阅硬编码测试替身),我们可以大大简化 Java 和 C# 等静态类型语言中手工构建测试替身的开发,并从中创建子类。伪对象可以将我们需要在每个测试桩测试间谍模拟对象中实现的方法数量减少到我们期望调用的方法数量。当我们使用内部测试替身自分流器(请参阅硬编码测试替身)时,它们特别有用。上例中使用的伪对象的类定义如下所示:

We can greatly simplify the development of Hand-Built Test Doubles in statically typed languages such as Java and C# by providing a set of base classes called Pseudo-Objects (see Hard-Coded Test Double) from which to create subclasses. Pseudo-Objects can reduce the number of methods we need to implement in each Test Stub, Test Spy, or Mock Object to just the ones we expect to be called. They are especially helpful when we are using Inner Test Doubles or Self Shunts (see Hard-Coded Test Double). The class definition for the Pseudo-Object used in the previous example looks like this:

  /**

  * 手工编码测试桩和模拟对象的基类

  */

public class PseudoTimeProvider implements ComplexTimeProvider {



      public Calendar getTime() throws TimeProviderEx {

            throw new PseudoClassException();

      }

      public Calendar getTimeDifference(Calendar baseTime,

                                                              Calendar otherTime)

                      throws TimeProviderEx {

            throw new PseudoClassException();

      }

      public Calendar getTime( String timeZone )

                      throws TimeProviderEx {

            throw new PseudoClassException();

      }

}

  /**

  *  Base  class  for  hand-coded  Test  Stubs  and  Mock  Objects

  */

public  class  PseudoTimeProvider  implements  ComplexTimeProvider  {



      public  Calendar  getTime()  throws  TimeProviderEx  {

            throw  new  PseudoClassException();

      }

      public  Calendar  getTimeDifference(Calendar  baseTime,

                                                              Calendar  otherTime)

                      throws  TimeProviderEx  {

            throw  new  PseudoClassException();

      }

      public  Calendar  getTime(  String  timeZone  )

                      throws  TimeProviderEx  {

            throw  new  PseudoClassException();

      }

}

 

配置测试替身

Configuring the Test Double

一些测试替身(具体来说,就是测试桩模拟对象)需要被告知要返回哪些值和/或期望哪些值。硬编码测试替身在设计时从测试自动化程序接收这些指令;而可配置测试替身则在运行时由测试将此信息告知(图 11.9)。测试桩测试间谍仅需配置 SUT 预计调用的方法将返回的值。模拟对象还需要配置我们预计 SUT 在其上调用的所有方法的名称和参数。在所有情况下,测试自动化程序最终决定使用哪些值来配置测试替身。毫不奇怪,做出这个决定时的主要考虑因素是测试的可理解性和测试替身代码的潜在可重用性。

Some Test Doubles (specifically, Test Stubs and Mock Objects) need to be told which values to return and/or which values to expect. A Hard-Coded Test Double receives these instructions at design time from the test automater; a Configurable Test Double is told this information at runtime by the test (Figure 11.9). A Test Stub or Test Spy needs to be configured only with the values that will be returned by the methods that the SUT is expected to invoke. A Mock Object also needs to be configured with the names and arguments of all methods we expect the SUT to invoke on it. In all cases, the test automater ultimately decides with which values to configure the Test Double. Not surprisingly, the primary considerations when making this decision are the understandability of the test and the potential reusability of the Test Double code.

图 11.9。 测试配置的测试替身。我们可以通过在运行时将返回值或期望传递给可配置测试替身来避免硬编码测试替身类的激增。

Figure 11.9. A Test Double being configured by the test. We can avoid a proliferation of Hard-Coded Test Doubles classes by passing return values or expectation to the Configurable Test Double at runtime.

图像

伪对象不需要在运行时进行“配置”,因为它们只是被 SUT 使用;后续输出取决于 SUT 之前的调用。同样,虚拟对象也不需要“配置”,因为它们永远不应该被执行。4程序 测试桩通常构建为硬编码测试替身。也就是说,它们被硬编码为在调用函数时返回特定值 — 因此它们是最简单的测试替身形式。

Fake Objects do not need to be "configured" at runtime because they are just used by the SUT; later outputs depend on the earlier calls by the SUT. Similarly, Dummy Objects do not need to be "configured" because they should never be executed.4 Procedural Test Stubs are typically built as Hard-Coded Test Doubles. That is, they are hard-coded to return a particular value when the function is called—thus they are the simplest form of Test Double.

可配置测试替身可以提供配置接口 (请参阅可配置测试替身配置模式 (请参阅可配置测试替身,测试可以使用它们来配置测试替身的返回值或预期值。因此,可配置测试替身可以在许多测试中重复使用。使用这些可配置测试替身还可以使测试更易于理解,因为测试替身使用的值在测试中是可见的,从而避免了神秘客人 的气味(请参阅 第 186页的模糊测试)。

A Configurable Test Double can provide either a Configuration Interface (see Configurable Test Double) or a Configuration Mode (see Configurable Test Double) that the test can use to configure the Test Double with the values to return or expect. As a consequence, Configurable Test Doubles are reusable across many tests. Use of these Configurable Test Doubles also makes tests more understandable because the values used by the Test Double are visible within the test, thus avoiding the smell of a Mystery Guest (see Obscure Test on page 186).

那么这个配置应该在哪里进行呢?测试替身的安装应该像夹具设置的其他部分一样对待。诸如内联设置第 408页)、隐式设置第 424页)和委托设置(第411页)等替代方案都可用。

So where should this configuration take place? The installation of the Test Double should be treated just like any other part of fixture setup. Alternatives such as In-line Setup (page 408), Implicit Setup (page 424), and Delegated Setup (page 411) are all available.

安装测试替身

Installing the Test Double

在执行 SUT 之前,我们需要“安装”测试所依赖的任何测试替身。这里的“安装”一词是描述告诉 SUT 使用测试替身的过程的通用方式,而不管我们如何执行此操作的具体细节。正常顺序是实例化测试替身,如果它是可配置测试替身,则对其进行配置,然后在执行 SUT 之前或执行 SUT 时告诉 SUT 使用测试替身。有几种不同的方法来“安装”测试替身,如果我们为可测试性设计 SUT,那么在它们之间进行选择可能既是风格问题,也是必要问题。然而,当我们试图将测试改进到现有设计时,我们的选择可能会受到更多限制。

Before we exercise the SUT, we need to "install" any Test Doubles on which our test depends. The term "install" here serves as a generic way to describe the process of telling the SUT to use our Test Double, regardless of the exact details regarding how we do it. The normal sequence is to instantiate the Test Double, configure it if it is a Configurable Test Double, and then tell the SUT to use the Test Double either before or as we exercise the SUT. There are several distinct ways to "install" the Test Double, and the choice between them may be as much a matter of style as of necessity if we are designing the SUT for testability. Our choices may be much more constrained, however, when we try to retrofit our tests to an existing design.

基本选择可以归结为依赖注入第 678页),其中客户端软件告诉 SUT 使用哪个 DOC;依赖查找第 686页),其中 SUT 将 DOC 的构造或检索委托给另一个对象;以及测试挂钩,其中 SUT 中的 DOC 或对它的调用被修改。

The basic choices boil down to Dependency Injection (page 678), in which the client software tells the SUT which DOC to use; Dependency Lookup (page 686), in which the SUT delegates the construction or retrieval of the DOC to another object; and Test Hook, in which the DOC or the calls to it within the SUT are modified.

如果我们的语言中有一个控制反转框架,我们的测试就可以替换依赖项,而无需我们做太多额外的工作。这样就不需要建立依赖注入依赖查找机制了。

If an inversion of control framework is available in our language, our tests can substitute dependencies without much additional work on our part. This removes the need for building in the Dependency Injection or Dependency Lookup mechanism.

依赖注入

依赖注入是一种设计解耦,其中客户端告诉 SUT 在运行时使用哪个 DOC(图 11.10测试驱动开发 (TDD) 运动大大提高了它的普及度,因为依赖注入使设计更容易测试。这种模式还可以更广泛地重用 SUT,因为它从 SUT 中移除了依赖关系的知识;通常 SUT 只会知道DOC 必须实现的通用接口。依赖注入有几种特定的风格,选择它们在很大程度上取决于个人喜好:

Dependency Injection is a class of design decoupling in which the client tells the SUT which DOC to use at runtime (Figure 11.10). The test-driven development (TDD) movement has greatly increased its popularity because Dependency Injection makes for more easily tested designs. This pattern also makes it possible to reuse the SUT more broadly because it removes knowledge of the dependency from the SUT; often the SUT will be aware of only a generic interface that the DOC must implement. Dependency Injection comes in several specific flavors, with the choice between them being largely a matter of taste:

  • 设置器注入(参见依赖注入 SUT 通过公共属性(即变量或属性)访问 DOC。测试在实例化 SUT 以安装测试替身后显式设置该属性。SUT 可能已在其构造函数中使用真实 DOC 初始化该属性(在这种情况下,测试正在替换它),或者 SUT 可以使用惰性初始化[SBPP]来初始化该属性(在这种情况下,SUT 不会费心安装真实 DOC)。
  • Setter Injection (see Dependency Injection): The SUT accesses the DOC through a public attribute (i.e., a variable or property). The test explicitly sets the attribute after instantiating the SUT to installing the Test Double. The SUT may have previously initialized the attribute with the real DOC in its constructor (in which case the test is replacing it) or the SUT may use Lazy Initialization [SBPP] to initialize the attribute (in which case the SUT will not bother to install the real DOC).
  • 构造函数注入(参见依赖注入 SUT 通过私有属性访问 DOC。测试通过构造函数将测试替身传递给 SUT,该构造函数将 DOC 用作显式参数并从中初始化属性。这可能是生产代码客户端使用的主构造函数,也可能是备用构造函数。在后一种情况下,主构造函数应调用此构造函数,并将默认 DOC 作为参数传递给它。
  • Constructor Injection (see Dependency Injection): The SUT accesses the DOC through a private attribute. The test passes the Test Double to the SUT via a constructor that takes the DOC to be used as an explicit argument and initializes the attribute from it. This may be the primary constructor used by production code clients or it may be an alternative constructor. In the latter case, the primary constructor should call this constructor, passing the default DOC to it as an argument.
  • 参数注入(参见依赖注入 SUT 接收 DOC 作为方法参数。测试传入测试替身,而生产代码传入真实对象。5SUT 的 API 将我们需要替换的对象作为参数时,此方法效果很好。尽管Mock Object爱好者可能会认为以这种方式设计 API 可以改进 SUT 的设计,但将所需的一切传递给每个方法并不总是可行或实用的。
  • Parameter Injection (see Dependency Injection): The SUT receives the DOC as a method parameter. The test passes in a Test Double, whereas the production code passes in the real object.5 This approach works well when the API of the SUT takes as a parameter the object we need to replace. Although Mock Object aficionados might argue that designing APIs in this way improves the design of the SUT, it is not always possible or practical to pass everything required to each method.

图 11.10。 测试替身被测试“注入”到 SUT 中。使用测试替身需要一种方法来替换 DOC。使用依赖注入需要调用者在使用之前或使用时向 SUT 提供依赖关系。

Figure 11.10. A Test Double being "injected" into the SUT by a test. Using Test Doubles requires a means to replace the DOC. Using Dependency Injection involves having the caller supply the dependency to the SUT before or as it is used.

图像

依赖项查找

当软件不是为可测试性而设计的,或者依赖注入不合适时,我们可能会发现使用依赖查找很方便。此模式还消除了 SUT 中关于应该使用哪个 DOC 的知识,但它通过让 SUT 要求另一个软件代表它创建或查找 DOC 来实现这一点(图 11.11。这为在运行时更改 DOC 打开了大门,而无需修改 SUT 的代码。我们确实必须以某种方式修改中介的行为,这就是依赖查找的具体变体彼此不同的地方:

When software is not designed for testability or when Dependency Injection is not appropriate, we may find it convenient to use Dependency Lookup. This pattern also removes the knowledge of exactly which DOC should be used from the SUT, but it does so by having the SUT ask another piece of software to create or find the DOC on its behalf (Figure 11.11). This opens the door to changing the DOC at runtime without modifying the SUT's code. We do have to modify the behavior of the intermediary somehow, and this is where the specific variants of Dependency Lookup differ from one another:

图 11.11。 服务定位器由测试“配置”以向 SUT 返回测试替身。使用测试替身需要一种方法来替换 DOC。使用依赖项查找需要让 SUT 请求一个众所周知的对象提供对 DOC 的引用;测试可以为服务定位器提供要返回的测试替身。

Figure 11.11. A Service Locator being "configured" by a test to return a Test Double to the SUT. Using Test Doubles requires a means to replace the DOC. Using Dependency Lookup involves having the SUT ask a well-known object to provide a reference to the DOC; the test can provide the Service Locator with a Test Double to return.

图像

当我们使用延迟初始化来创建服务定位器返回的对象时,这两种模式之间的界限会变得非常模糊。它应该被称为对象工厂吗?我们应用哪个标签真的很重要吗?可能不重要——因此有了依赖查找的通用名称。

The line between these two patterns can become quite blurry when we use Lazy Initialization to create the object being returned by a Service Locator. Should it be called an Object Factory instead? Does it really matter which label we apply? Probably not—hence the generic name of Dependency Lookup.

使用特定于测试的子类改进可测试性

即使 SUT 中没有内置这些​​机制,我们也可以通过使用特定于测试的子类相对轻松地对其进行改造。

Even when none of these mechanisms is built into the SUT, we may be able to retrofit them relatively easily by using a Test-Specific Subclass.

使用单例[GOF]专门充当对象工厂服务定位器是很常见的。如果单例具有硬编码行为,我们可能必须将其转换为可替代单例(请参阅第 579页的测试特定子类),以便使用我们的测试替身覆盖通常返回的 DOC 。可以通过使用IOC工具或手动编码的依赖注入机制来避免使用单例。这两种选择都是可取的,因为它们使测试对测试替身的依赖更加明显。用于其他目的的单例几乎总是会在我们编写测试时引起麻烦,应尽可能避免。

The use of Singletons [GOF] specifically to act as an Object Factory or Service Locator is common. If the Singleton has hard-coded behavior, we may have to turn it into a Substitutable Singleton (see Test-Specific Subclass on page 579) to enable overriding the normally returned DOC with our Test Double. The use of Singletons can be avoided through the use of an IOC tool or a manually coded Dependency Injection mechanism. Both of these choices are preferable because they make the test's dependency on a Test Double more obvious. Singletons used for other purposes almost always cause headaches when we are writing tests and should be avoided if possible.

我们的测试可以实例化SUT 的测试特定子类,以添加依赖注入机制或用测试特定行为替换 SUT 的其他方法;参见图 11.12。我们可以覆盖用于访问 DOC 的任何逻辑,从而可以在不修改生产代码的情况下返回测试替身而不是普通 DOC。我们还可以用类似测试桩的行为替换从我们正在测试的方法中调用的任何方法的实现,从而将 SUT 转变为其自己的子类测试替身(参见测试特定子类)。这是向 SUT 注入间接输入的一种方法。

Our test can instantiate a Test-Specific Subclass of the SUT to add a Dependency Injection mechanism or to replace other methods of the SUT with test-specific behavior; see Figure 11.12. We can override any logic used to access a DOC, thereby making it possible to return a Test Double instead of the normal DOC without modifying the production code. We can also replace the implementations of any methods being called from the method we are testing with Test Stub-like behavior, thereby turning the SUT into its own Subclassed Test Double (see Test-Specific Subclass). This is one way to inject indirect inputs into the SUT.

图 11.12。 使用 SUT 的测试特定子类。当所有其他方法都失败时,我们总是可以尝试对 SUT 进行子类化,以更改或公开我们需要启用测试的功能

Figure 11.12. Using a Test-Specific Subclass of the SUT. When all else fails, we can always try subclassing the SUT to change or expose functionality we need to enable testing

图像

使用 SUT 的测试特定子类的主要先决条件是 SUT 必须使用自调用[WWW]来调用非私有方法,这些方法实现了我们需要从测试中覆盖的任何功能。小型、单一用途的方法规则!这种方法的主要缺点是可能会意外覆盖我们打算测试的行为的一部分。

The main prerequisite of using a Test-Specific Subclass of the SUT is that the SUT must use Self-Calls [WWW] to nonprivate methods that implement any functionality we need to override from the test. Small, single-purpose methods rule! The main drawback of this approach is that it is possible to accidentally override parts of the behavior we are intending to test.

我们还可以对 DOC 进行子类化,以插入特定于测试的行为,从而有效地将其转变为子类化测试替身图 11.13)。此策略比对 SUT 进行子类化更安全,因为它避免了意外覆盖我们正在测试的 SUT 部分的可能性。但是,诀窍是让 SUT 使用测试特定子类而不是 DOC。实际上,这意味着我们必须使用依赖注入依赖查找技术之一,除非 DOC 是单例。当 SUT 通过调用soleInstance硬编码类名上的静态方法使用单例时,测试可以通过对单例类进行子类化并初始化真实单例的类变量以保存测试soleInstance替身的实例,从而使该方法返回测试替身的实例。如果用于保存单例唯一实例的变量类型被硬编码为单例的类,则返回的测试替身可能需要是子类化测试替身。虽然我们经常使用这种技术来让服务定位器返回不同的服务,但我们也可以直接使用子类测试替身,而不需要中介服务定位器soleInstance

We can also subclass the DOC to insert test-specific behavior, effectively turning it into a Subclassed Test Double (Figure 11.13). This strategy is somewhat safer than subclassing the SUT because it avoids the possibility of accidentally overriding those parts of the SUT that we are testing. The trick, however, is to get the SUT to use the Test-Specific Subclass instead of the DOC. In practice, this implies that we must use one of the Dependency Injection or Dependency Lookup techniques, unless the DOC is a Singleton. When the SUT uses a Singleton by calling a static soleInstance method on a hard-coded class name, the test can cause the soleInstance method to return an instance of a Test Double by subclassing the Singleton class and initializing the real Singleton's soleInstance class variable to hold an instance of the Test Double. The returned Test Double may need to be a Subclassed Test Double if the type of the variable used to hold the Singleton's sole instance is hard-coded as the Singleton's class. Although we often use this technique to get a Service Locator to return a different service, but we can also use a Subclassed Test Double directly without an intermediary Service Locator.

图 11.13。 使用从 DOC 中子类化的测试替身。构建测试替身的一种方法是对真实类进行子类化,并重写我们需要控制间接输入或验证间接输出的任何方法的实现。

Figure 11.13. Using A Test Double subclassed from the DOC. One way to build a Test Double is to subclass the real class and override the implementation of any methods we need to control the indirect inputs or verify indirect outputs.

图像

改进可测试性的其他方法

即使到目前为止所描述的技术都无法用于引入可测试性,也并不意味着一切都失败了。我们还有一些小窍门。

All is not lost when none of the techniques described thus far can be used to introduce testability. We still have a few tricks left up our sleeves.

测试钩子是“房间里的大象”,没有人愿意谈论它,因为它们可能会导致生产中的测试逻辑。然而,当引入前面描述的技术之一太难或太危险时,测试钩子是一种完全合法的测试遗留代码的方法。它们最好用作“过渡”策略,以允许脚本测试(第285)或记录测试第 278)自动化,以提供安全网(参见第24),同时进行大规模重构以提高可测试性。理想情况下,一旦代码变得更易于测试,就可以使用前面描述的技术准备更好的测试,并且可以删除测试钩子。

Test Hooks are the "elephant in the room" that no one wants to talk about because they may lead to Test Logic in Production. Test Hooks, however, are a perfectly legitimate way to get legacy code under test when it is too hard or dangerous to introduce one of the techniques described earlier. They are best used as a "transition" strategy to allow Scripted Tests (page 285) or Recorded Tests (page 278) to be automated to provide a Safety Net (see page 24) while large-scale refactoring is undertaken to improve testability. Ideally, once the code has been made more testable, better tests can be prepared using the techniques described earlier and the Test Hooks can be removed.

Michael Feathers [WEwLC]描述了几种其他技术,用于在“对象接缝”这一大标题下用测试专用代码替换依赖项。例如,我们可以用专门为测试设计的库替换依赖库。这样就可以打破看似硬编码的依赖关系。当我们需要在单个测试中动态替换依赖项时,这些技术中的大多数都不如依赖注入依赖查找那么适用,因为它们需要更改环境。然而,对象接缝是将遗留代码置于测试之下的绝佳方式,这样就可以对其进行重构,以引入前面提到的任何一种依赖打破技术。

Michael Feathers [WEwLC] has described several other techniques to replace dependencies with test-specific code under the general heading of finding "object seams." For example, we can replace a depended-on library with a library designed specifically for testing. A seemingly hard-coded dependency can be broken this way. Most of these techniques are less applicable when we need to dynamically replace dependencies within individual tests than either Dependency Injection or Dependency Lookup because they require changes to the environment. Object seams are, however, an excellent way to place legacy code under test so that it can be refactored to introduce either of the previously mentioned dependency-breaking techniques.

我们可以使用面向方面编程 (AOP) 来安装测试替身行为,方法是定义一个测试切入点,该切入点与 SUT 调用 DOC 的位置相匹配,而我们更希望它调用测试替身。虽然我们需要一个支持 AOP 的开发环境来执行此操作,但我们不需要将 AOP 生成的代码部署到生产环境中。因此,即使在 AOP 不利的环境中也可以使用此技术。

We can use aspect-oriented programming (AOP) to install the Test Double behavior by defining a test point-cut that matches the place where the SUT calls the DOC and we would rather have it call the Test Double. Although we need an AOP-enabled development environment to do this, we do not need to deploy the AOP-generated code into a production environment. As a consequence, this technique may be used even in AOP-hostile environments.

测试替身的其他用途

Other Uses of Test Doubles

到目前为止,我们已经介绍了间接输入和间接输出的测试。现在让我们看看测试替身的其他一些用途。

So far, we have covered the testing of indirect inputs and indirect outputs. Now let's look at some other uses of Test Doubles.

内窥镜检查

Endoscopic Testing

Tim Mackinnon 等人在他们最初的Mock Objects论文中引入了内窥镜测试 [ET]的概念。内窥镜测试侧重于通过将Mock Object作为参数传递给被测方法从内部测试 SUT 。这允许验证 SUT 的某些内部行为,而这些行为可能并不总是从外部可见。

Tim Mackinnon et al. introduced the concept of endoscopic testing [ET] in their initial Mock Objects paper. Endoscopic testing focuses on testing the SUT from the inside by passing in a Mock Object as an argument to the method under test. This allows verification of certain internal behaviors of the SUT that may not always be visible from the outside.

Mackinnon 及其同事引用的经典示例是使用预加载了所有预期集合成员的模拟集合类。当SUT尝试添加意外成员时,模拟集合的断言会失败。然后,内部调用堆栈的完整堆栈跟踪将在 xUnit 故障报告中可见。如果我们的 IDE 支持在指定异常时中断,我们还可以在故障点检查局部变量。

The classic example that Mackinnon and colleagues cite is the use of a mock collection class preloaded with all of the expected members of the collection. When the SUT tries to add an unexpected member, the mock collection's assertion fails. The full stack trace of the internal call stack then becomes visible in the xUnit failure report. If our IDE supports breaking on specified exceptions, we can also inspect the local variables at the point of failure.

需求驱动开发

Need-Driven Development

内窥镜测试的一个改进是“需求驱动开发” [MRNO],其中 SUT 的依赖关系在编写测试时定义。这种“由外而内”的软件编写和测试方法将传统的“自上而下”代码编写方法的概念优雅与Mock Objects支持的现代 TDD 技术相结合。它允许我们逐层构建和测试软件,从最外层开始,然后再实现较低的层。

A refinement of endoscopic testing is "need-driven development" [MRNO], in which the dependencies of the SUT are defined as the tests are written. This "outside-in" approach to writing and testing software combines the conceptual elegance of the traditional "top-down" approach to writing code with modern TDD techniques supported by Mock Objects. It allows us to build and test the software layer by layer, starting at the outermost layer before we have implemented the lower layers.

需求驱动开发结合了测试驱动开发(在构建软件之前指定所有软件的测试)和高度增量的设计方法的优点,从而无需对依赖类的使用方式进行任何猜测

Need-driven development combines the benefits of test-driven development (specifying all software with tests before we build them) with a highly incremental approach to design that removes the need for any speculation about how a depended-on class might be used.

加快夹具设置速度

Speeding Up Fixture Setup

测试替身的另一个应用是减少Fresh Fixture (第 311页) 设置的运行时成本。当 SUT 需要与其他难以创建的对象交互时,可以创建单个测试替身,而不是复杂的对象网络。当应用于实体对象网络时,此技术称为实体链剪切(请参阅测试桩)。

Another application of Test Doubles is to reduce the runtime cost of Fresh Fixture (page 311) setup. When the SUT needs to interact with other objects that are difficult to create because they have many dependencies, a single Test Double can be created instead of the complex network of objects. When applied to networks of entity objects, this technique is called Entity Chain Snipping (see Test Stub).

加速测试执行

Speeding Up Test Execution

测试替身也可用于加速测试,方法是用更快的组件替换慢的组件。例如,伪对象伪数据库被减少的等待时间和由于更频繁地运行测试而产生的更及时的反馈所抵消。有关此问题的更详细讨论,319无需共享装置即可加快测试速度

Test Doubles may also be used to speed up tests by replacing slow components with faster ones. Replacing a relational database with an in-memory Fake Object, for example, can reduce test execution times by an order of magnitude! The extra effort required to code the Fake Database is more than offset by the reduced waiting time and the quality improvement due to the more timely feedback that comes from running the tests more frequently. Refer to the sidebar "Faster Tests without Shared Fixtures" on page 319 for a more detailed discussion of this issue.

其他考虑因素

Other Considerations

由于我们的许多测试将涉及用测试替身替换真实 DOC ,那么我们如何知道生产代码在使用真实 DOC 时会正常工作呢?当然,我们希望客户测试能够验证使用真实 DOC 时的行为(除非真实 DOC 是其他系统的接口,需要在单系统测试期间将其桩)。我们应该编写一种特殊形式的构造函数测试(参见测试方法)——“可替代的初始化测试”——来验证真实 DOC 是否安装正确。编写此测试的触发因素是执行第一个用测试替身替换 DOC 的测试——此时通常是引入测试替身安装机制的时候。

Because many of our tests will involve replacing a real DOC with a Test Double, how do we know that the production code will work properly when it uses the real DOC? Of course, we would expect our customer tests to verify behavior with the real DOCs in place (except, possibly, when the real DOCs are interfaces to other systems that need to be stubbed out during single-system testing). We should write a special form of Constructor Test (see Test Method)—a "substitutable initialization test"—to verify that the real DOC is installed properly. The trigger for writing this test is performing the first test that replaces the DOC with a Test Double—that point is often when the Test Double installation mechanism is introduced.

最后,我们要小心,不要落入“新锤子陷阱”。6过度使用测试替身(尤其是模拟对象测试桩)会导致过度指定的软件(请参阅第239页的“脆弱测试”),因为在我们的测试中编码了有关设计的实现特定信息。如果许多测试仅仅因为使用了受设计更改影响的测试替身而受到更改的影响,那么设计可能就更难更改。

Finally, we want to be careful that we don't fall into the "new hammer trap."6 Overuse of Test Doubles (and especially Mock Objects or Test Stubs) can lead to Overspecified Software (see Fragile Test on page 239) by encoding implementation-specific information about the design in our tests. The design may be then much more difficult to change if many tests are affected by the change simply because they use a Test Double that has been affected by the design change.

下一步是什么?

What's Next?

在本章中,我们研究了使用间接输入和间接输出测试软件的技术。特别是,我们探讨了测试替身的概念和安装它们的各种技术。在第12 章“组织我们的测试”中,我们将关注将测试代码组织成在测试用例第 373页)和测试助手(第 643页)上实现的测试方法和测试实用方法(第599页)的策略

In this chapter, we examined techniques for testing software with indirect inputs and indirect outputs. In particular, we explored the concept of Test Doubles and various techniques for installing them. In Chapter 12, Organizing Our Tests, we will turn our attention to strategies for organizing the test code into Test Methods and Test Utility Methods (page 599) implemented on Testcase Classes (page 373) and Test Helpers (page 643).

第 12 章

组织我们的测试

Chapter 12

Organizing Our Tests

 

关于本章

About This Chapter

在以第 11 章“使用测试替身”为结尾的章节中,我们研究了与 SUT 交互以验证其行为的各种技术。在本章中,我们将注意力转向如何组织测试代码以使其易于查找和理解的问题。

In the chapters concluding with Chapter 11, Using Test Doubles, we looked at various techniques for interacting with the SUT for the purpose of verifying its behavior. In this chapter, we turn our attention to the question of how to organize the test code to make it easy to find and understand.

测试代码组织的基本单位是测试方法第 348页)。决定在测试方法中放入什么内容以及将其放在哪里是测试组织主题的核心。当我们只有几个测试时,如何组织它们并不重要。相反,当我们有数百个测试时,测试组织就成为保持测试易于理解和查找的关键因素。

The basic unit of test code organization is the Test Method (page 348). Deciding what to put in the Test Method and where to put it is central to the topic of test organization. When we have only a few tests, how we organize them isn't terribly important. By contrast, when we have hundreds of tests, test organization becomes a critical factor in keeping our tests easy to understand and find.

本章首先讨论测试方法中应该包含和不应该包含的内容。接下来,本章探讨如何决定将测试方法放在哪个测试用例类(第 373页) 中。测试命名在很大程度上取决于我们如何组织测试,因此我们将在下文讨论这个问题。然后,我们将考虑如何将测试用例类组织到测试套件中以及将测试代码放在何处。最后一个主题是测试代码重用 — 具体来说,将可重用的测试代码放在何处。

This chapter begins by discussing what we should and should not include in a Test Method. Next, it explores how we can decide on which Testcase Classes (page 373) to put our Test Methods. Test naming depends heavily on how we have organized our tests, so we will talk about this issue next. We will then consider how to organize the Testcase Classes into test suites and where to put test code. The final topic is test code reuse—specifically, where to put reusable test code.

基本 xUnit 机制

Basic xUnit Mechanisms

xUnit 系列测试自动化框架(第 298页) 提供了许多功能来帮助我们组织测试。基本问题“我应该在哪里编写测试代码?”的答案是将测试代码放入测试用例类的测试方法中。然后,我们使用测试发现(第 393页) 或测试枚举(第 399页) 创建一个包含测试用例类中所有测试的测试套件对象(第 387页) 。测试运行器(第 377页) 调用测试套件对象上的方法来运行所有测试方法

The xUnit family of Test Automation Frameworks (page 298) provides a number of features to help us organize our tests. The basic question, "Where do I code my tests?", is answered by putting our test code into a Test Method on a Testcase Class. We then use either Test Discovery (page 393) or Test Enumeration (page 399) to create a Test Suite Object (page 387) containing all the tests from the Testcase Class. The Test Runner (page 377) invokes a method on the Test Suite Object to run all the Test Methods.

合适规模的测试方法

Right-Sizing Test Methods

测试条件是我们需要证明 SUT 确实能做到的事情;它可以通过以下方面来描述:SUT 的起始状态是什么、我们如何执行 SUT、我们期望 SUT 如何响应以及 SUT 的预期结束状态是什么。测试方法是测试脚本语言中的一系列语句,用于执行一个或多个测试条件(图 12.1)。我们应该在单个测试方法中包含哪些内容?

A test condition is something we need to prove the SUT really does; it can be described in terms of what the starting state of the SUT is, how we exercise the SUT, how we expect the SUT to respond, and what the ending state of the SUT is expected to be. A Test Method is a sequence of statements in our test scripting language that exercises one or more test conditions (Figure 12.1). What should we include in a single Test Method?

图 12.1. 典型测试的四个阶段。每种测试方法都实施四阶段测试(第358页),理想情况下可验证单个测试条件。四阶段测试的所有阶段不需要包含在测试方法中。

Figure 12.1. The four phases of a typical test. Each Test Method implements a Four-Phase Test (page 358) that ideally verifies a single test condition. Not all phases of the Four-Phase Test need be in the Test Method.

图像

许多 xUnit 纯粹主义者更喜欢“每个测试验证一个条件”(参见第45页),因为这可以帮助他们很好地定位缺陷(参见第22页)。也就是说,当测试失败时,他们确切地知道 SUT 中出了什么问题,因为每个测试都只验证一个测试条件。这与手动测试形成了鲜明的对比,手动测试往往会构建冗长、复杂的多条件测试,因为设置每个测试的先决条件会产生开销。在创建基于 xUnit 的自动化测试时,我们有很多方法可以处理这种频繁重复的夹具设置(如第 8 章瞬态夹具管理”中所述),因此我们倾向于“每个测试验证一个条件”。我们将验证了太多测试条件的测试称为Eager 测试(参见第224页的断言轮盘赌),并将其视为代码异味。

Many xUnit purists prefer to Verify One Condition per Test (see page 45) because it gives them good Defect Localization (see page 22). That is, when a test fails, they know exactly what is wrong in the SUT because each test verifies exactly one test condition. This is very much in contrast with manual testing, where one tends to build long, involved multiple-condition tests because of the overhead involved in setting up each test's pre-conditions. When creating xUnit-based automated tests, we have many ways of dealing with this frequently repeated fixture setup (as described in Chapter 8, Transient Fixture Management), so we tend to Verify One Condition per Test. We call a test that verifies too many test conditions an Eager Test (see Assertion Roulette on page 224) and consider it a code smell.

验证单个测试条件的测试将执行 SUT 中的一条代码路径,并且每次运行时都应执行完全相同的路径;这就是可重复测试(参见第26页)的原因。是的,这意味着我们需要与代码路径一样多的测试方法 - 但否则我们怎样才能期望实现完整的代码覆盖率?这种模式易于管理的原因在于,我们在为每个类编写单元测试时隔离了 SUT(参见第43页),因此我们只需关注通过单个对象的路径。此外,由于每个测试应该只验证代码中的一条路径,因此每个测试方法都应由严格顺序的语句组成,这些语句描述在该路径上应该发生什么。1我们每个测试验证一个条件(参见第45页)的另一个原因是为了最大限度地减少测试重叠(参见第44页),以便如果我们稍后修改 SUT 的行为,则需要修改的测试更少。

A test that verifies a single test condition executes a single code path through the SUT and it should execute exactly the same path each time it runs; that is what makes it a Repeatable Test (see page 26). Yes, that means we need as many test methods as we have paths through the code—but how else can we expect to achieve full code coverage? What makes this pattern manageable is that we Isolate the SUT (see page 43) when we write unit tests for each class so we only have to focus on paths through a single object. Also, because each test should verify only a single path through the code, each test method should consist of strictly sequential statements that describe what should happen on that one path.1 Another reason we Verify One Condition per Test (see page 45) is to Minimize Test Overlap (see page 44) so that we have fewer tests to modify if we later modify the behavior of the SUT.

Brian Marrick 开发了一种有趣的折衷方案,我称之为“While We're at It” 2,它利用我们已经设置的测试装置来运行一些额外的检查和断言。Marrick 用注释清楚地标记了这些元素,以表明如果对 SUT 的更改使该部分测试过时,则可以轻松删除它们。此策略最大限度地减少了维护额外测试代码所需的工作量。

Brian Marrick has developed an interesting compromise that I call "While We're at It,"2 which leverages the test fixture we already have set up to run some additional checks and assertions. Marrick clearly marks these elements with comments to indicate that if changes to the SUT obsolete that part of the test, they can be easily deleted. This strategy minimizes the effort needed to maintain the extra test code.

测试方法和测试用例类

Test Methods and Testcase Classes

测试方法需要存在于测试用例类中。我们是否应该将所有测试方法都放在应用程序的单个测试用例类中?还是应该为每个测试方法创建一个测试用例类?当然,正确的答案介于这两个极端之间,并且会随着项目的生命周期而变化。

A Test Method needs to live on a Testcase Class. Should we put all our Test Methods onto a single Testcase Class for the application? Or should we create a Testcase Class for each Test Method? Of course, the right answer lies somewhere between these two extremes, and it will change over the life of our project.

每个类的测试用例类

Testcase Class per Class

当我们编写最初的几个测试方法时,我们可以将它们全部放在一个测试用例类中。随着测试方法数量的增加,我们可能希望拆分测试用例类,以便每个类(第 617页)测试一个测试用例类,从而减少每个类的测试方法数量(图 12.2)。当这些测试用例类变得太大时,我们通常会进一步拆分类。在这种情况下,我们需要决定在每个测试用例类中包含哪些测试方法

When we write our first few Test Methods, we can put them all onto a single Testcase Class. As the number of Test Methods increases, we will likely want to split the Testcase Class so that one Testcase Class per Class (page 617) is tested, which reduces the number of Test Methods per class (Figure 12.2). As those Testcase Classes get too big, we usually split the classes further. In that case, we need to decide which Test Methods to include in each Testcase Class.

图 12.2. 具有单个测试用例类的生产类。使用每个类一个测试用例类模式,单个测试用例类包含我们 SUT 类的所有行为的所有测试方法。每个测试方法可能需要创建不同的装置,可以内联创建,也可以将该任务委托给创建方法(第415页)。

Figure 12.2. A production class with a single Testcase Class. With the Testcase Class per Class pattern, a single Testcase Class holds all the Test Methods for all the behavior of our SUT class. Each Test Method may need to create a different fixture either in-line or by delegating that task to a Creation Method (page 415).

图像

每个功能的测试用例类

Testcase Class per Feature

一种思路是将验证 SUT 特定功能的所有测试方法(其中“功能”定义为共同实现 SUT 某些功能的一种或几种方法和属性)放入单个测试用例类中(图 12.3)。这样可以轻松查看该功能的所有测试条件。(使用适当的测试命名约定有助于实现这种清晰度。)但是,这可能导致每个测试用例类都需要类似的装置设置代码。

One school of thought is to put all Test Methods that verify a particular feature of the SUT—where a "feature" is defined as one or more methods and attributes that collectively implement some capability of the SUT—into a single Testcase Class (Figure 12.3). This makes it easy to see all test conditions for that feature. (Use of appropriate Test Naming Conventions helps achieve this clarity.) It can, however, result in similar fixture setup code being required in each Testcase Class.

图 12.3。 每个功能都有一个测试用例类的生产类。使用每个功能一个测试用例类模式,我们为 SUT 类支持的每个主要功能或特性都有一个测试用例类。该测试类上的测试方法在构建所需的任何测试装置后,会练习该功能的各个方面。

Figure 12.3. A production class with one Testcase Class for each feature. With the Testcase Class per Feature pattern, we have one Testcase Class for each major capability or feature supported by our SUT class. The Test Methods on that test class exercise various aspects of that feature after building whatever test fixture they require.

图像

每个装置的测试用例类

Testcase Class per Fixture

相反的观点是,应该将需要相同测试装置(相同先决条件)的所有测试方法分组到每个装置的一个测试用例类中(第631页;参见图 12.4)。这有利于将测试装置设置代码放入setUp方法中(隐式设置参见第424页),但可能导致每个功能的测试条件分散在许多测试用例类中。

The opposing view is that one should group all Test Methods that require the same test fixture (same pre-conditions) into one Testcase Class per Fixture (page 631; see Figure 12.4). This facilitates putting the test fixture setup code into the setUp method (Implicit Setup; see page 424) but can result in scattering of the test conditions for each feature across many Testcase Classes.

图 12.4. 每个装置都有一个测试用例类的生产类。使用每个装置一个测试用例类模式,我们为 SUT 类的每个可能的测试装置(测试先决条件)都有一个测试用例类。该测试类上的测试方法从共同的起点出发,运用各种特性。

Figure 12.4. A production class with one Testcase Class for each fixture. With the Testcase Class per Fixture pattern, we have one Testcase Class for each possible test fixture (test pre-condition) of our SUT class. The Test Methods on that test class exercise various features from the common starting point.

图像

选择测试方法组织策略

Choosing a Test Method Organization Strategy

显然,没有一种“最佳实践”可以一直遵循;最佳实践是最适合特定情况的实践。当我们为有状态对象编写单元测试,并且需要在对象的每个状态下测试每个方法时,通常使用每个装置一个测试用例类。我们针对服务外观[CJ2EEP]编写客户测试时,每个功能一个测试用例类第 624页)更为合适;它使我们能够将客户可识别功能的所有测试放在一起。当我们依赖预构建装置第 429页)时,这种模式也更常用,因为每个测试中不需要装置设置逻辑。当每个测试都需要略有不同的装置时,正确的答案可能是选择每个功能一个测试用例类模式并使用委托设置(第411页)来方便设置装置。

Clearly, there is no single "best practice" we can always follow; the best practice is the one that is most appropriate for the particular circumstance. Testcase Class per Fixture is commonly used when we are writing unit tests for stateful objects and each method needs to be tested in each state of the object. Testcase Class per Feature (page 624) is more appropriate when we are writing customer tests against a Service Facade [CJ2EEP]; it enables us to keep all the tests for a customer-recognizable feature together. This pattern is also more commonly used when we rely on a Prebuilt Fixture (page 429) because fixture setup logic is not required in each test. When each test needs a slightly different fixture, the right answer may be to select the Testcase Class per Feature pattern and use a Delegated Setup (page 411) to facilitate setting up the fixtures.

测试命名约定

Test Naming Conventions

我们为测试用例类测试方法指定的名称对于让我们的测试易于查找和理解至关重要。我们可以根据每个测试方法验证的测试条件对其进行系统命名,从而使测试覆盖率更加明显。无论我们使用哪种测试方法组织方案,我们都希望测试包、测试用例类测试方法的名称组合至少传达以下信息:

The names we give to our Testcase Classes and Test Methods are crucial in making our tests easy to find and understand. We can make the test coverage more obvious by naming each Test Method systematically based on which test condition it verifies. Regardless of which test method organization scheme we use, we would like the combination of the names of the test package, the Testcase Class, and the Test Method to convey at least the following information:

  • SUT 类的名称
  • The name of the SUT class
  • 正在执行的方法或功能的名称
  • The name of the method or feature being exercised
  • 与 SUT 运行相关的任何输入值的重要特征
  • The important characteristics of any input values related to the exercising of the SUT
  • 与 SUT 状态或其依赖项相关的任何信息
  • Anything relevant about the state of the SUT or its dependencies

这些项目是测试条件的“输入”部分。显然,仅用两个名称来传达这些信息是不够的,但如果我们能做到这一点,回报将非常丰厚:我们只需在 IDE 的大纲视图中查看类和方法的名称,就可以准确地知道我们要测试哪些测试条件。图 12.5提供了一个例子。

These items are the "input" part of the test condition. Obviously, this is a lot to communicate in just two names but the reward is high if we can achieve it: We can tell exactly what test conditions we have tests for merely by looking at the names of the classes and methods in an outline view of our IDE. Figure 12.5 provides an example.

图 12.5. 每个测试装置都有一个测试用例类的生产类。当我们使用每个装置一个测试用例类模式时,类名可以描述装置,而方法名则可用于描述输入和预期输出。

Figure 12.5. A production class with one Testcase Class for each test fixture. When we use the Testcase Class per Fixture pattern, the class name can describe the fixture, leaving the method name available for describing the inputs and expected outputs.

图像

图 12.5还显示了包含测试条件的“期望”方面有多么有用:

Figure 12.5 also shows how useful it is to include the "expectations" side of the test condition:

  • 执行 SUT 时预期的输出(响应)
  • The outputs (responses) expected when exercising the SUT
  • SUT 的预期运动后状态及其依赖关系
  • The expected post-exercise state of the SUT and its dependencies

此信息可以包含在以“应该”为前缀的测试方法名称中。如果此命名法使名称太长,3我们始终可以通过查看测试方法的主体来访问预期结果。

This information can be included in the name of the Test Method prefixed by "should." If this nomenclature makes the names too long,3 we can always access the expected outcome by looking at the body of the Test Method.

组织测试套件

Organizing Test Suites

Testcase在返回包含测试用例对象集合(第 382页)的测试套件对象时充当测试套件工厂(参见测试枚举) ,每个测试用例对象代表一种测试方法图 12.6)。这是 xUnit 提供的默认组织机制。大多数测试运行器允许任何类通过实现工厂方法[GOF](通常称为)充当测试套件工厂suite

The Testcase Class acts as a Test Suite Factory (see Test Enumeration) when it returns a Test Suite Object containing a collection of Testcase Objects (page 382), each representing a Test Method (Figure 12.6). This is the default organization mechanism provided by xUnit. Most Test Runners allow any class to act as a Test Suite Factory by implementing a Factory Method [GOF], which is typically called suite.

图 12.6. 充当测试套件工厂的测试用例类。默认情况下,测试用例类充当测试套件工厂,以生成测试运行器执行测试所需的测试套件对象。我们还可以通过提供测试套件工厂来枚举我们想要运行的一组特定测试,该工厂返回仅包含所需测试的测试套件对象。

Figure 12.6. A Testcase Class acting as a Test Suite Factory. By default, the Testcase Class acts as a Test Suite Factory to produce the Test Suite Object that the Test Runner requires to execute our tests. We can also enumerate a specific set of tests we want to run by providing a Test Suite Factory that returns a Test Suite Object containing only the desired tests.

图像

运行测试组

Running Groups of Tests

我们经常想要运行一组测试(即测试套件),但是我们不想这个决定限制我们如何组织它们。一种流行的惯例是为每个测试包创建一个特殊的测试套件工厂AllTests。然而,我们不必就此止步:我们可以为任何想要一起运行的测试集合创建命名测试套件(第 592页) 。子集套件(请参阅命名测试套件) 就是一个很好的例子,它允许我们只运行那些需要将软件部署到 Web 服务器(或不部署到 Web 服务器!)的测试。我们通常至少为所有单元测试准备一个子集套件,为客户测试准备另一个子集套件(它们通常需要很长时间才能执行)。某些 xUnit 变体支持测试选择(第 403页),我们可以使用它来代替定义子集套件

We often want to run groups of tests (i.e., a test suite) but we don't want this decision to constrain how we organize them. A popular convention is to create a special Test Suite Factory called AllTests for each package of tests. We don't need to stop there, however: We can create Named Test Suites (page 592) for any collection of tests we want to run together. A good example is a Subset Suite (see Named Test Suite) that allows us to run just those tests that need software deployed to the Web server (or not deployed to the Web server!). We usually have at least a Subset Suite for all the unit tests and another Subset Suite for just the customer tests (they often take a long time to execute). Some variants of xUnit support Test Selection (page 403), which we can use instead of defining Subset Suites.

此类运行时测试分组通常反映了测试需要运行的环境。例如,我们可能有一个子集套件,其中包含所有可以在没有数据库的情况下运行的测试,另一个子集套件则包含所有依赖于数据库的测试。同样,我们可能为依赖和不依赖 Web 服务器的测试分别设置单独的子集套件AllTests。如果我们的测试包包含这些不同类型的测试套件,我们可以将这些子集套件定义为套件套件(请参阅测试套件对象。然后,任何添加到子集套件之一的测试也将在其中运行,AllTests而无需额外的测试维护工作。

Such runtime groupings of tests often reflect the environment in which they need to run. For example, we might have one Subset Suite that includes all tests that can be run without the database and another Subset Suite that includes all tests that depend on the database. Likewise, we might have separate Subset Suites for tests that do, and do not, rely on the Web server. If our test package includes these various kinds of test suites, we can define AllTests as a Suite of Suites (see Test Suite Object) composed of these Subset Suites. Then any test that is added to one of the Subset Suites will also be run in AllTests without incurring extra test maintenance effort.

运行单个测试

Running a Single Test

假设测试用例类中的测试方法失败我们决定在某个方法上设置断点 — 但该方法在每个测试中都会被调用。我们的第一反应可能是每次遇到断点时都单击“Go”,直到从感兴趣的测试中调用我们。一种可能性是禁用(通过注释掉)其他测试方法,这样它们就不会运行。另一种选择是重命名其他测试方法,这样 xUnit测试发现机制就不会将它们识别为测试。在使用方法属性或注释的 xUnit 变体中,我们可以将“Ignore”属性添加到测试方法中。这些方法中的每一种都引入了丢失测试的潜在问题(请参阅第268页的生产错误尽管“Ignore”方法确实提醒我们某些测试被忽略了。在提供测试树资源管理器(请参阅测试运行器)的 xUnit 系列成员中,我们可以简单地从测试套件的层次结构视图中选择要运行的单个测试,如图12.7所示。

Suppose a Test Method fails in our Testcase Class. We decide to put a breakpoint on a particular method—but that method is called in every test. Our first reaction might be to just muddle through by clicking "Go" each time the breakpoint is hit until we are being called from the test of interest. One possibility is to disable (by commenting out) the other Test Methods so they are not run. Another option is to rename the other Test Methods so that the xUnit Test Discovery mechanism will not recognize them as tests. In variants of xUnit that use method attributes or annotations, we can add the "Ignore" attribute to a test method instead. Each of these approaches introduces the potential problem of a Lost Test (see Production Bugs on page 268), although the "Ignore" approach does remind us that some tests are being ignored. In members of the xUnit family that provide a Test Tree Explorer (see Test Runner), we can simply select a single test to be run from the hierarchy view of the test suite, as shown in Figure 12.7.

图 12.7。 测试树资源管理器显示了套件中的测试结构。我们可以使用测试树资源管理器深入研究测试套件的运行时结构并运行单个测试或子套件。

Figure 12.7. A Test Tree Explorer showing the structure of the tests in our suite. We can use the Test Tree Explorer to drill down into the runtime structure of the test suite and run individual tests or subsuites.

图像

当这些选项都不可用时,我们可以使用测试套件工厂来运行单个测试。等一下!测试套件不都是运行位于不同测试用例类中的测试组吗?嗯,是的,但这并不意味着我们不能将它们用于其他目的。我们可以定义一个运行特定测试的单个测试套件4(参见命名测试套件)。为此,我们调用测试用例类的构造函数,并以特定测试方法的名称作为参数。

When none of these options is available, we can use a Test Suite Factory to run a single test. Wait a minute! Aren't test suites all about running groups of tests that live in different Testcase Classes? Well, yes, but that doesn't mean we can't use them for other purposes. We can define a Single Test Suite4 (see Named Test Suite) that runs a particular test. To do so, we call the constructor of the Testcase Class with the specific Test Method's name as an argument.

测试代码重用

Test Code Reuse

测试代码重复(第 213) 会显著增加编写和维护测试的成本。幸运的是,我们可以使用多种技术来重用测试逻辑。最重要的考虑是,任何重用都不能损害测试作为文档的价值(参见第23)。我不建议在不同情况下重用实际的测试方法(例如,使用不同的夹具),因为这种重用通常是一种灵活测试(参见第 200页的条件测试逻辑),它会在不同情况下测试不同的东西。大多数测试代码重用是通过隐式设置测试实用方法(第599页) 实现的。主要的例外是许多测试对测试替身(第 522的重用在考虑将它们放在哪里时,我们可以将这些测试替身类视为一种特殊的测试助手(第643

Test Code Duplication (page 213) can significantly increase the cost of writing and maintaining tests. Luckily, a number of techniques for reusing test logic are available to us. The most important consideration is that any reuse not compromise the value of the Tests as Documentation (see page 23). I don't recommend reuse of the actual Test Method in different circumstances (e.g., with different fixtures), as this kind of reuse is typically a sign of a Flexible Test (see Conditional Test Logic on page 200) that tests different things in different circumstances. Most test code reuse is achieved either through Implicit Setup or Test Utility Methods (page 599). The major exception is the reuse of Test Doubles (page 522) by many tests; we can treat these Test Double classes as a special kind of Test Helper (page 643) when thinking about where to put them.

测试实用程序方法位置

Test Utility Method Locations

xUnit 的许多变体都提供了一个特殊的测试用例超类第 638页)——通常称为“TestCase”——所有测试用例类都应该(在某些情况下必须)直接或间接地从该超类继承(图 12.8 )。如果我们的测试用例类中有想要在其他测试用例类中重用的实用方法,则创建一个或多个测试用例超类来继承而不是从“TestCase”会很有帮助。如果我们采取这一步骤,我们需要小心,如果这些方法需要查看 SUT 中各个包中的类型或类——我们的根测试用例超类不应该直接依赖于这些类型或类,因为这可能会导致循环依赖关系图。我们可以为每个测试包创建一个测试用例超类,以保持我们的测试类依赖关系不是循环的。另一种方法是为每个域包创建一个测试助手,并将各种测试助手放在适当的测试包中。这样,测试用例类就不会被迫选择单个测试用例超类;它只能“使用”适当的测试助手

Many variants of xUnit provide a special Testcase Superclass (page 638)—typically called "TestCase"—from which all Testcase Classes should (and, in some cases, must) inherit either directly or indirectly (Figure 12.8). If we have useful utility methods on our Testcase Class that we want to reuse in other Testcase Classes, we may find it helpful to create one or more Testcase Superclasses from which to inherit instead of "TestCase." If we take this step, we need to be careful if those methods need to see types or classes that reside in various packages within the SUT—our root Testcase Superclass should not depend on those types or classes directly, as that is likely to result in a cyclical dependency graph. We may be able to create a Testcase Superclass for each test package to keep our test class dependencies noncyclic. The alternative is to create a Test Helper for each domain package and put the various Test Helpers in the appropriate test packages. This way, a Testcase Class is not forced to choose a single Testcase Superclass; it can merely "use" the appropriate Test Helpers.

图 12.8. 我们可以放置测试实用方法的各个位置。主要的决策标准是测试方法的可重用性期望范围。

Figure 12.8. The various places we can put Test Utility Methods. The primary decision-making criterion is the desired scope of reusability of the Test Methods.

图像

测试用例的继承和重用

TestCase Inheritance and Reuse

从测试用例超类继承方法的最常用原因是访问测试实用程序方法。另一个用途是测试框架及其插件;创建一个一致性测试来指定插件的一般行为会很有用,该测试通过模板方法[GOF]调用特定于被测试插件类型的子类提供的方法来检查插件的具体细节。这种情况很少见,我不会在这里进一步描述它;请参阅[FaT]了解更完整的描述。

The most commonly used reason for inheriting methods from a Testcase Superclass is to access Test Utility Methods. Another use is when testing frameworks and their plug-ins; it can be useful to create a conformance test that specifies the general behavior of the plug-in via a Template Method [GOF] that calls methods provided by a subclass specific to the kind of plug-in being tested to check specific details of the plug-in. This scenario is rare enough that I won't describe it further here; please refer to [FaT] for a more complete description.

测试文件组织

Test File Organization

现在我们面临一个新问题:我们应该把测试用例类放在哪里?显然,这些类应该与生产代码一起存储在源代码存储库 [SCM] 中。除了这个标准之外,我们还有相当多的选择。我们选择的测试打包策略在很大程度上取决于我们的环境 — 许多 IDE 包含一些限制,这些限制使得某些策略无法实施。关键问题是将测试逻辑置于生产代码之外(参见第45页),同时还要能够找到每段代码或功能的相应测试。

Now we face a new question: Where should we put our Testcase Classes? Obviously, these classes should be stored in the source code repository [SCM] along with the production code. Beyond that criterion, we have quite a range of choices. The test packaging strategy we choose will very much depend on our environment—many IDEs include constraints that make certain strategies unworkable. The key issue is to Keep Test Logic Out of Production Code (see page 45) and yet to be able to find the corresponding test for each piece of code or functionality.

内置自检

Built-in Self-Test

使用内置自测试,测试包含在生产代码中,可随时运行。没有规定将它们分开。许多组织希望将测试逻辑排除在生产代码之外,因此内置自测试对他们来说可能不是一个好的选择。这种考虑在内存受限的环境中尤其重要,因为我们不希望测试代码占用宝贵的空间。

With a built-in self-test, the tests are included with the production code and can be run at any time. No provision is made for keeping them separate. Many organizations want to Keep Test Logic Out of Production Code so built-in self-tests may not be a good option for them. This consideration is particularly important in memory-constrained environments where we don't want test code taking up valuable space.

一些开发环境鼓励我们将测试和生产代码放在一起。例如,SAP 的ABAP Unit支持关键字“For Testing”,该关键字告诉系统在将代码传输到生产环境时禁用测试。

Some development environments encourage us to keep the tests and the production code together. For example, SAP's ABAP Unit supports the keyword "For Testing," which tells the system to disable the tests when the code is transported into the production environment.

测试包

Test Packages

如果我们决定将测试用例类放入单独的测试包中,我们可以通过多种方式组织它们。我们可以将测试放入一个或多个测试包中,同时将它们保存在同一源树中,从而保持测试的独立性,或者我们可以将测试放入同一个逻辑包中,但将它们物理存储在并行源树中。后一种方法在 Java 中经常使用,因为它避免了测试无法在 SUT 上看到“受包保护”的方法的问题。5一些IDE 可能会拒绝使用这种方法,坚持要求包完全包含在单个文件夹或项目中。当我们在每个生产代码包下使用测试包时,我们可能需要使用构建时测试剥离器将它们从生产构建中排除。

If we decide to put the Testcase Classes into separate test packages, we can organize them in several ways. We can keep the tests separate by putting them into one or more test packages while keeping them in the same source tree, or we can put the tests into the same logical package but physically store them in a parallel source tree. The latter approach is frequently used in Java because it avoids the problem of tests not being able to see "package-protected" methods on the SUT.5 Some IDEs may reject using this approach by insisting that a package be wholly contained within a single folder or project. When we use test packages under each production code package, we may need to use a build-time test stripper to exclude them from production builds.

测试依赖项

Test Dependencies

不管我们决定如何存储和管理源代码,我们都需要确保消除生产中的任何测试依赖关系(请参阅第 217页的生产中的测试逻辑),因为如果生产代码需要测试才能运行,即使是测试剥离器也无法删除测试。这一要求使得关注类依赖关系变得重要。我们也不希望在生产中有任何测试逻辑,因为这意味着我们测试的代码与最终将在生产中运行的代码不同。第6 章“测试自动化策略”将更详细地讨论这个问题

However we decide to store and manage the source code, we need to ensure that we eliminate any Test Dependency in Production (see Test Logic in Production on page 217) because even a test stripper cannot remove the tests if production code needs them to be present to run. This requirement makes paying attention to our class dependencies important. We also don't want to have any Test Logic in Production because it means we aren't testing the same code that we will eventually run in production. This issue is discussed in more detail in Chapter 6, Test Automation Strategy.

下一步是什么?

What's Next?

现在我们已经了解了如何组织测试代码,我们应该熟悉更多的测试模式。这些模式在第 13 章使用数据库进行测试”中介绍。

Now that we've looked at how to organize our test code, we should become familiar with a few more testing patterns. These patterns are introduced in Chapter 13, Testing with Databases.

第 13 章

使用数据库进行测试

Chapter 13

Testing with Databases

 

关于本章

About This Chapter

第 12 章“组织我们的测试”中,我们研究了组织测试代码的技术。在本章中,我们将探讨应用程序包含数据库时出现的问题。带有数据库的应用程序在编写自动化测试时会带来一些特殊挑战。数据库比现代计算机中使用的处理器慢得多。因此,与数据库交互的测试往往比完全在内存中运行的测试运行得慢得多。

In Chapter 12, Organizing Our Tests, we looked at techniques for organizing our test code. In this chapter, we explore the issues that arise when our application includes a database. Applications with databases present some special challenges when writing automated tests. Databases are much slower than the processors used in modern computers. As a result, tests that interact with databases tend to run much, much more slowly than tests that can run entirely in memory.

即使忽略慢速测试第 253页)的可能性,数据库也是我们自动化测试套件中许多测试异味的来源。其中一些异味是数据库持久性特性的直接结果,而另一些则源于我们选择在测试之间共享装置实例。这些异味在第 9 章持久装置管理”中介绍过。本章对它们进行了扩展,并提供了更有针对性的数据库测试处理。

Even ignoring the potential for Slow Tests (page 253), databases are a ripe source for many test smells in our automated test suites. Some of these smells are a direct consequence of the persistent nature of the database, while others result from our choice to share the fixture instance between tests. These smells were introduced in Chapter 9, Persistent Fixture Management. This chapter expands on them and provides a more focused treatment of testing with databases.

使用数据库进行测试

Testing with Databases

这是我针对这个问题提出的第一个也是最重要的建议:

Here is my first, and most critical, piece of advice on this subject:

如果有任何方法可以在不使用数据库的情况下进行测试,那就不使用数据库进行测试!

When there is any way to test without a database, test without the database!

这似乎是一条非常有力的建议,但这样表述是有原因的。数据库给我们的应用程序带来了各种各样的麻烦,尤其是给我们的测试带来了麻烦。需要数据库的测试运行速度平均比没有数据库的测试慢两个数量级。

This seems like pretty strong advice but it is phrased this way for a reason. Databases introduce all sorts of complications into our applications and especially into our tests. Tests that require a database run, on average, two orders of magnitude slower than the same tests that run without a database.

为什么要使用数据库进行测试?

Why Test with Databases?

许多应用程序都包含数据库,用于将对象或数据持久保存到长期存储中。数据库是应用程序的必要组成部分,因此验证数据库是否正确使用是构建应用程序的必要部分。因此,使用数据库沙箱(第 650页)将开发人员和测试人员与生产环境(以及彼此之间)隔离是几乎每个项目的基本做法(图 13.1)。

Many applications include a database to persist objects or data into longer-term storage. The database is a necessary part of the application, so verifying that the database is used properly is a necessary part of building the application. Therefore, the use of a Database Sandbox (page 650) to isolate developers and testers from production (and each other) is a fundamental practice on almost every project (Figure 13.1).

图 13.1. 每位开发人员一个数据库沙箱。开发人员共享一个数据库沙箱是错误的节约。你会让水管工和电工同时在同一面墙里工作吗?

Figure 13.1. A Database Sandbox for each developer. Sharing a Database Sandbox among developers is false economy. Would you make a plumber and an electrician work in the same wall at the same time?

图像

数据库问题

Issues with Databases

数据库引入了许多使测试自动化复杂化的问题。其中许多问题与夹具的持久性有关。这些问题在第 9 章“持久夹具管理”中介绍过,并在此进行了简要总结。

A database introduces a number of issues that complicate test automation. Many of these issues relate to the fact that the fixture is persistent. These issues were introduced in Chapter 9, Persistent Fixture Management, and are summarized briefly here.

持久装置

在编写自动化测试时,带有数据库的应用程序会带来一些特殊的挑战。数据库比现代计算机的处理器慢得多。因此,与数据库交互的测试往往比完全在内存中运行的测试运行得慢得多。但即使忽略测试速度慢的问题,数据库也是自动化测试套件中测试异味的主要来源。常见的异味包括不稳定的 测试(第228页)和模糊的测试第 186页)。由于数据库中的数据可能会在测试运行结束后很长时间内仍然存在,因此我们必须特别注意这些数据,避免创建只能运行一次的测试或相互交互的测试。这些不可重复的测试(请参阅不稳定的测试)和交互测试(请参阅不稳定的测试)是测试装置持久性的直接后果,并且随着应用程序的发展,可能导致测试维护成本更高。

Applications with databases present some special challenges when we are writing automated tests. Databases are much slower than the processors used in modern computers. As a consequence, tests that interact with a database tend to run much more slowly than tests that can run entirely in memory. But even ignoring the Slow Tests issue, databases are a prime source of test smells in our automated test suites. Commonly encountered smells include Erratic Tests (page 228) and Obscure Tests (page 186). Because the data in a database may potentially persist long after we run our test, we must pay special attention to this data to avoid creating tests that can be run only once or tests that interact with one another. These Unrepeatable Tests (see Erratic Test) and Interacting Tests (see Erratic Test) are a direct consequence of the persistence of the test fixture and can result in more expensive maintenance of our tests as the application evolves.

共享装置

持久化基座是一回事,选择共享又是另一回事。如果某些测试依赖于其他测试来为它们设置基座,那么故意共享基座可能会导致孤独测试(请参阅不稳定的测试),这种情况称为链式测试第 454页)。如果我们没有为每个开发人员提供自己的数据库沙箱,我们可能会在开发人员之间引发测试运行战争(请参阅不稳定的测试)。当两个或多个测试运行器第 377页)运行的测试通过访问共享数据库实例中的相同基座对象而进行交互时,就会出现这个问题。这些行为异味都是共享测试基座决定的直接后果。持久化的程度和基座共享的范围直接影响这些异味的存在与否。

Persistence of the fixture is one thing; choosing to share it is another. Deliberate sharing of the fixture can result in Lonely Tests (see Erratic Test) if some tests depend on other tests to set up the fixture for them—a situation called Chained Tests (page 454). If we haven't provided each developer with his or her own Database Sandbox, we might spark a Test Run War (see Erratic Test) between developers. This problem arises when the tests being run from two or more Test Runners (page 377) interact by virtue of their accessing the same fixture objects in the shared database instance. Each of these behavior smells is a direct consequence of the decision to share the test fixture. The degree of persistence and the scope of fixture sharing directly affect the presence or absence of these smells.

一般设施

依赖数据库的测试的另一个问题是,数据库往往会演变成一个大型的通用夹具(请参阅模糊测试),许多测试会将其用于不同的目的。当我们使用预建夹具第 429页)来避免在每个测试中设置夹具时,尤其有可能导致这种结果。当我们采用新鲜夹具第 311页)策略时,决定使用标准夹具(第305页)也会导致这种情况。这种方法很难确定每个测试到底在指定什么。实际上,数据库在所有测试中都以神秘嘉宾(请参阅模糊测试)的形式出现。

Another problem with tests that rely on databases is that databases tend to evolve into a large General Fixture (see Obscure Test) that many tests use for different purposes. This outcome is particularly likely when we use a Prebuilt Fixture (page 429) to avoid setting up the fixture in each test. It can also result from the decision to use a Standard Fixture (page 305) when we employ a Fresh Fixture (page 311) strategy. This approach makes it difficult to determine exactly what each test is specifying. In effect, the database appears as a Mystery Guest (see Obscure Test) in all of the tests.

不使用数据库进行测试

Testing without Databases

现代分层软件架构[DDDPEAAWWW]开启了完全不使用数据库即可测试业务逻辑的可能性。我们可以通过使用层测试第 337页)并用测试替身(第522页)替换数据访问层,将业务逻辑层与系统的其他层隔离开来测试;参见图 13.2

Modern layered software architecture [DDD, PEAA, WWW] opens up the possibility of testing the business logic without using the database at all. We can test the business logic layer in isolation from the other layers of the system by using Layer Tests (page 337) and replacing the data access layer with a Test Double (page 522); see Figure 13.2.

图 13.2。 一对层测试,每个层测试系统的不同层。层测试允许我们独立于其他层构建每个层。当持久层可以用测试替身代替时,它们特别有用,从而降低测试的上下文敏感性(请参阅第 239页的脆弱性测试)。

Figure 13.2. A pair of Layer Tests, each of which tests a different layer of the system. Layer Tests allow us to build each layer independently of the other layers. They are especially useful when the persistence layer can be replaced by a Test Double that reduces the Context Sensitivity (see Fragile Test on page 239) of the tests.

图像

如果我们的架构分层程度不足以进行层测试,我们仍然可以通过使用假数据库(请参阅第551页的假对象)或内存数据库(请参阅假对象)来在没有真实数据库的情况下进行测试。内存数据库是一个数据库,但将其表存储在内存中;这种结构使其运行速度比基于磁盘的数据库快得多。假数据库实际上根本不是数据库;它是一个假装是数据库的数据访问层。通常,使用假数据库更容易确保测试的独立性,因为我们通常会在夹具设置逻辑中创建一个新的夹具,从而实现瞬态新鲜夹具(请参阅新鲜夹具)策略。尽管如此,这两种策略都允许我们的测试以内存速度运行,从而避免慢测试。只要我们继续将测试编写为往返测试,就不会引入太多有关 SUT 结构的知识。

If our architecture is not sufficiently layered to allow for Layer Tests, we may still be able to test without a real database by using either a Fake Database (see Fake Object on page 551) or an In-Memory Database (see Fake Object). An In-Memory Database is a database but stores its tables in memory; this structure makes it run much faster than a disk-based database. A Fake Database isn't really a database at all; it is a data access layer that merely pretends to be one. As a rule, it is easier to ensure independence of tests by using a Fake Database because we typically create a new one as part of our fixture setup logic, thereby implementing a Transient Fresh Fixture (see Fresh Fixture) strategy. Nevertheless, both of these strategies allow our tests to run at in-memory speeds, thereby avoiding Slow Tests. We don't introduce too much knowledge of the SUT's structure as long as we continue to write our tests as round-trip tests.

只要我们只将数据库用作数据存储库,用测试替身替换数据库的效果就很好。如果我们使用任何供应商特定的功能,比如序列号生成或存储过程,事情就会变得更有趣。替换数据库就会变得更有挑战性,因为它需要更多地关注可测试性设计。一般策略是将所有数据库交互封装在数据访问层中。如果数据访问层提供数据访问功能,我们可以简单地将这些任务委托给“数据库对象”。我们必须为实现供应商特定功能的数据访问层接口的任何部分提供测试特定的实现——测试桩第 529页)非常适合这项任务。

Replacing the database with a Test Double works well as long as we use the database only as a data repository. Things get more interesting if we use any vendor-specific functionality, such as sequence number generation or stored procedures. Replacing the database then becomes a bit more challenging because it requires more attention to creating a design for testability. The general strategy is to encapsulate all database interaction within the data access layer. Where the data access layer provides data access functionality, we can simply delegate these duties to the "database object." We must provide test-specific implementations for any parts of the data access layer interface that implement the vendor-specific functionality—a task for which a Test Stub (page 529) fits the bill nicely.

如果我们要利用特定于供应商的数据库功能(例如序列号生成),则需要在内存中执行测试时提供此功能。通常,我们不需要用测试替身替换任何与功能相关的对象,因为该功能在数据库的后台发生。我们可以使用策略[GOF]对象将此功能添加到应用程序的内存版本中,该对象默认初始化为空对象[PLOPD3]。在生产中运行时,空对象不执行任何操作;在内存中运行时,策略对象提供缺少的功能。作为额外的好处,一旦我们采取了这一步骤,我们将发现更容易更改为其他数据库供应商,因为提供此功能的钩子已经存在。1

If we are taking advantage of vendor-specific database features such as sequence number generation, we will need to provide this functionality when executing the tests in memory. Typically, we will not need to substitute a Test Double for any functionality-related object because the functionality happens behind the scenes within the database. We can add this functionality into the in-memory version of the application using a Strategy [GOF] object, which by default is initialized to a null object [PLOPD3]. When run in production, the null object does nothing; when run in memory, the strategy object provides the missing functionality. As an added benefit, we will find it easier to change to a different database vendor once we have taken this step because the hooks to provide this functionality already exist.1

通过自动化测试替换数据库(或数据访问层)意味着我们有办法指示 SUT 使用替换对象。这通常通过以下两种方式之一完成:通过直接依赖注入(第678页)或确保业务逻辑层使用依赖查找(第686页)来查找数据访问层。

Replacing the database (or the data access layer) via an automated test implies that we have a way to instruct the SUT to use the replacement object. This is commonly done in one of two ways: through direct Dependency Injection (page 678) or by ensuring that the business logic layer uses Dependency Lookup (page 686) to find the data access layer.

测试数据库

Testing the Database

假设我们已经找到了不使用数据库来测试大多数软件的方法,那么会怎样呢?测试数据库的需要就消失了吗?当然不是!我们应该确保数据库正常运行,就像我们编写的任何其他软件一样。但是,我们可以专注于测试数据库逻辑,以减少需要编写的测试数量和种类。由于涉及数据库的测试比内存测试运行速度慢得多,因此我们希望将这些测试的数量保持在最低限度。

Assuming we have found ways to test most of our software without using a database, then what? Does the need to test the database disappear? Of course not! We should ensure that the database functions correctly, just like any other software we write. We can, however, focus our testing of the database logic so as to reduce the number and kinds of tests we need to write. Because tests that involve the database will run much more slowly than our in-memory tests, we want to keep the number of these tests to the bare minimum.

我们需要哪种数据库测试?这个问题的答案取决于我们的应用程序如何使用数据库。如果我们有存储过程,我们应该编写单元测试来验证它们的逻辑。如果数据访问层将数据库隐藏在业务逻辑之外,我们应该为数据访问功能编写测试。

What kinds of database tests will we require? The answer to this question depends on how our application uses the database. If we have stored procedures, we should write unit tests to verify their logic. If a data access layer hides the database from the business logic, we should write tests for the data access functionality.

测试存储过程

Testing Stored Procedures

我们可以采用两种方式之一编写存储过程测试。远程存储过程测试(请参阅存储过程测试)使用与编写其他所有单元测试相同的编程语言和框架编写。它通过与应用程序逻辑中使用的相同调用机制(即通过某种远程代理[GOF]、外观[GOF]或命令对象[GOF])访问存储过程。或者,我们可以采用与存储过程本身相同的语言编写数据库内存储过程测试(请参阅存储过程测试);这些测试将在数据库内运行(图 13.3)。xUnit 系列成员适用于几种最常见的存储过程语言;utPLSQL只是其中一个例子。

We can write tests for stored procedures in one of two ways. A Remote Stored Procedure Test (see Stored Procedure Test on page 654) is written in the same programming language and framework as we write all of our other unit tests. It accesses the stored procedure via the same invocation mechanism as used within the application logic (i.e., by some sort of Remote Proxy [GOF], Facade [GOF], or Command object [GOF]). Alternatively, we can write In-Database Stored Procedure Tests (see Stored Procedure Test) in the same language as the stored procedure itself; these tests will run inside the database (Figure 13.3). xUnit family members are available for several of the most common stored procedure languages; utPLSQL is just one example.

图 13.3. 使用自检测试测试存储过程(见第 26页)。对存储过程进行自动回归测试具有很大的价值,但我们必须注意使它们可重复且健壮。

Figure 13.3. Testing a stored procedure using Self-Checking Tests (see page 26). There is great value in having automated regression test for stored procedures, but we must take care to make them repeatable and robust.

图像

测试数据访问层

Testing the Data Access Layer

我们还想为数据访问层编写一些单元测试。在大多数情况下,这些数据访问层测试可以是往返测试。尽管如此,进行一些跨层测试还是很有用的,以确保我们将信息放入正确的列中。这可以使用用于数据库测试的 xUnit 框架扩展(例如,Java 版 DbUnit)来完成,以将数据直接插入数据库(用于“读取”测试)或验证数据库的测试后内容(用于“创建/更新/删除”测试)。

We also want to write some unit tests for the data access layer. For the most part, these data access layer tests can be round-trip tests. Nevertheless, it is useful to have a few layer-crossing tests to ensure that we are putting information into the correct columns. This can be done using xUnit framework extensions for database testing (e.g., DbUnit for Java) to insert data directly into the database (for "Read" tests) or to verify the post-test contents of the database (for "Create/Update/Delete" tests).

一个有用的技巧是使用事务回滚拆卸第 668页),可以防止我们的基座在数据访问层测试期间变得持久。为此,我们在构建数据访问层时依赖于Humble 事务控制器(请参阅第 695页的Humble 对象DFT模式。也就是说,读取或写入数据库的代码永远不应提交事务;这允许通过回滚事务的测试来执行代码,以防止应用 SUT 所做的任何更改。

A useful trick for keeping our fixture from becoming persistent during data access layer testing is to use Transaction Rollback Teardown (page 668). To do so, we rely on the Humble Transaction Controller (see Humble Object on page 695) DFT pattern when constructing our data access layer. That is, the code that reads or writes the database should never commit a transaction; this allows the code to be exercised by a test that rolls back the transaction to prevent any of the changes made by the SUT from being applied.

另一种方法是删除在测试的夹具设置和练习 SUT 阶段对数据库所做的任何更改,即表截断删除第 661页)。这种删除数据的“强力”技术仅在每个开发人员都有自己的数据库沙箱并且我们想要清除一个或多个表中的所有数据时才有效。

Another way to tear down any changes made to the database during the fixture setup and exercise SUT phases of the test is Table Truncation Teardown (page 661). This "brute force" technique for deleting data works only when each developer has his or her own Database Sandbox and we want to clear out all the data in one or more tables.

确保开发人员的独立性

Ensuring Developer Independence

测试数据库意味着我们需要有真实的数据库来运行这些测试。在此测试过程中,每个开发人员都需要有自己 数据库沙箱。试图在几个或所有开发人员之间共享一个沙箱是一种错误的节约;开发人员最终只会互相绊倒并浪费大量时间。2听过许多不给每个开发人员自己的沙箱的各种借口,但坦率地说,这些借口都站不住脚。最合理的担忧与每个开发人员的数据库许可证成本有关——但即使是这个障碍也可以通过选择其中一种“虚拟沙箱”变体来克服。如果数据库技术支持,我们可以使用每个 TestRunner 的 DB 模式(参见数据库沙箱);否则,我们必须使用数据库分区方案(参见数据库沙箱)。

Testing the database means we need to have the real database available for running these tests. During this testing process, every developer needs to have his or her own Database Sandbox. Trying to share a single sandbox among several or all developers is a false economy; the developers will simply end up tripping over one another and wasting a lot of time.2 I have heard many different excuses for not giving each developer his or her own sandbox, but frankly none of them holds water. The most legitimate concern relates to the cost of a database license for each developer—but even this obstacle can be surmounted by choosing one of the "virtual sandbox" variations. If the database technology supports it, we can use a DB Schema per TestRunner (see Database Sandbox); otherwise, we have to use a Database Partitioning Scheme (see Database Sandbox).

使用数据库进行测试(再次!)

Testing with Databases (Again!)

假设我们已经很好地完成了系统分层,并实现了无需访问真实数据库即可运行大多数测试的目标。那么,我们应该针对真实数据库运行哪些类型的测试呢?答案很简单:“尽可能少,但不能更少!”实际上,我们希望针对数据库运行至少一个具有代表性的客户测试样本,以确保 SUT 在有数据库和没有数据库的情况下的行为方式相同。除非某些特定的用户界面功能依赖于数据库,否则这些测试不需要通过用户界面访问业务逻辑;在大多数情况下,皮下测试(参见分层测试)应该足够了。

Suppose we have done a good job layering our system and achieved our goal of running most of our tests without accessing the real database. Now what kinds of tests should we run against the real database? The answer is simple: "As few as possible, but no fewer!" In practice, we want to run at least a representative sample of our customer tests against the database to ensure that the SUT behaves the same way with a database as without one. These tests need not access the business logic via the user interface unless some particular user interface functionality depends on the database; Subcutaneous Tests (see Layer Test) should be adequate in most circumstances.

下一步是什么?

What's Next?

在本章中,我们研究了使用数据库进行测试的特殊技术。这一讨论仅仅触及了敏捷软件开发与数据库之间交互的表面。3 第 14 章有效测试自动化路线图”总结了我们迄今为止所涵盖的内容,并就项目团队应如何加快开发人员测试自动化提出了一些建议。

In this chapter, we looked at special techniques for testing with databases. This discussion has merely scratched the surface of the interactions between agile software development and databases.3 Chapter 14, A Roadmap to Effective Test Automation, summarizes the material we have covered thus far and makes some suggestions about how a project team should come up to speed on developer test automation.

第 14 章

有效测试自动化的路线图

Chapter 14

A Roadmap to Effective Test Automation

 

关于本章

About This Chapter

第 13 章使用数据库进行测试”介绍了一组特定于测试具有数据库的应用程序的模式。这些模式基于第 6 章测试自动化策略”第 9 章持久性装置管理”;和第 11 章“使用测试替身”中。在我们能够有效地使用和不使用数据库进行测试之前,我们需要熟悉大量的材料!

Chapter 13, Testing with Databases, introduced a set of patterns specific to testing applications that have a database. These patterns built on the techniques described in Chapter 6, Test Automation Strategy; Chapter 9, Persistent Fixture Management; and Chapter 11, Using Test Doubles. This was a lot of material to become familiar with before we could test effectively with and without databases!

这引出了一个重要的观点:我们不可能一夜之间就成为测试自动化专家——这些技能需要时间来培养。学习我们可以使用的各种工具和模式也需要时间。本章提供了如何学习模式和掌握技能的路线图。它介绍了“测试自动化成熟度”的概念,该概念大致基于 SEI 的能力成熟度模型 (CMM)。

This raises an important point: We don't become experts in test automation overnight—these skills take time to develop. It also takes time to learn the various tools and patterns at our disposal. This chapter provides something of a roadmap for how to learn the patterns and acquire the skills. It introduces the concept of "test automation maturity," which is loosely based on the SEI's Capability Maturity Model (CMM).

测试自动化难度

Test Automation Difficulty

有些类型的测试比其他类型的测试更难编写。这种困难部分是因为技术更复杂,部分是因为它们不太为人所知,而且用于进行这种测试自动化的工具也不太容易获得。以下常见类型的测试按难度大致顺序列出,从最简单到最困难:

Some kinds of tests are harder to write than others. This difficulty arises partly because the techniques are more involved and partly because they are less well known and the tools to do this kind of test automation are less readily available. The following common kinds of tests are listed in approximate order of difficulty, from easiest to most difficult:

  1. 简单实体对象(领域模型[PEAA]

    • 简单的业务类,没有依赖关系

     

    •具有依赖关系的复杂业务类

     
  2. Simple entity objects (Domain Model [PEAA])

    • Simple business classes with no dependencies

     

    • Complex business classes with dependencies

     
  3. 无状态服务对象

    • 通过组件测试单独组件

     

    • 通过层测试测试整个业务逻辑层(第 337页)

     
  4. Stateless service objects

    • Individual components via component tests

     

    • The entire business logic layer via Layer Tests (page 337)

     
  5. 有状态的服务对象

    • 客户通过服务门面[CJ2EEP]使用皮下测试进行测试(参见层测试

     

    • 通过组件测试实现有状态的组件

     
  6. Stateful service objects

    • Customer tests via a Service Facade [CJ2EEP] using Subcutaneous Tests (see Layer Test)

     

    • Stateful components via component tests

     
  7. “难以测试”的代码

    • 通过Humble Dialog公开的用户界面逻辑(请参阅第 695页的Humble 对象

     

    • 数据库逻辑

     

    • 多线程软件

     
  8. "Hard-to-test" code

    • User interface logic exposed via Humble Dialog (see Humble Object on page 695)

     

    • Database logic

     

    • Multi-threaded software

     
  9. 面向对象的遗留软件(未经任何测试构建的软件)
  10. Object-oriented legacy software (software built without any tests)
  11. 非面向对象的遗留软件
  12. Non-object-oriented legacy software

随着我们继续往下看,软件测试变得越来越具有挑战性。具有讽刺意味的是,许多团队通过尝试将测试改造到现有应用程序上而“初出茅庐”。这使他们成为此列表中最后两类之一,而这正是需要最多经验的地方。不幸的是,许多团队未能成功测试遗留软件,这可能会使他们对尝试自动化测试产生偏见,无论是否采用测试驱动开发。如果您发现自己试图通过将测试改造到遗留软件上来学习测试自动化,我有两条建议给您:首先,聘请一位曾经做过这件事的人来帮助您完成这个过程。其次,阅读 Michael Feathers 的优秀著作[WEwLC];他介绍了许多专门适用于改造测试的技术。

As we move down this list, the software becomes increasingly more challenging to test. The irony is that many teams "get their feet wet" by trying to retrofit tests onto an existing application. This puts them in one of the last two categories in this list, which is precisely where the most experience is required. Unfortunately, many teams fail to test the legacy software successfully, which may then prejudice them against trying automated testing, with or without test-driven development. If you find yourself trying to learn test automation by retrofitting tests onto legacy software, I have two pieces of advice for you: First, hire someone who has done it before to help you through this process. Second, read Michael Feathers' excellent book [WEwLC]; he covers many techniques specifically applicable to retrofitting tests.

高度可维护的自动化测试路线图

Roadmap to Highly Maintainable Automated Tests

鉴于某些类型的测试比其他类型的测试更难编写,在继续编写更困难的测试之前,先专注于学习编写较容易的测试是有意义的。在向开发人员教授自动化测试时,我按以下顺序介绍这些技术。该路线图基于马斯洛的需求层次理论[HoN],该理论认为,只有在满足了较低级别的需求后,我们才会努力满足更高级别的需求。

Given that some kinds of tests are much harder to write than others, it makes sense to focus on learning to write the easier tests first before we move on to the more difficult kinds of tests. When teaching automated testing to developers, I introduce the techniques in the following sequence. This roadmap is based on Maslow's hierarchy of needs [HoN], which says that we strive to meet the higher-level needs only after we have satisfied the lower-level needs.

  1. 练习快乐路径代码

    • 设置 SUT 的简单预测试状态

     

    • 通过调用被测试方法来练习 SUT

     
  2. Exercise the happy path code

    • Set up a simple pre-test state of the SUT

     

    • Exercise the SUT by calling the method being tested

     
  3. 验证快乐路径的直接输出

    •在 SUT 的响应上调用断言方法第 362页)

     

    •在测试后状态下调用断言方法

     
  4. Verify direct outputs of the happy path

    • Call Assertion Methods (page 362) on the SUT's responses

     

    • Call Assertion Methods on the post-test state

     
  5. 验证替代路径

    • 改变 SUT 方法参数

     

    • 改变 SUT 的测试前状态

     

    • 通过测试桩控制 SUT 的间接输入(第 529页)

     
  6. Verify alternative paths

    • Vary the SUT method arguments

     

    • Vary the pre-test state of the SUT

     

    • Control indirect inputs of the SUT via a Test Stub (page 529)

     
  7. 验证间接输出行为

    • 使用Mock 对象第 544页)或测试间谍第 538页)拦截并验证传出的方法调用

     
  8. Verify indirect output behavior

    • Use Mock Objects (page 544) or Test Spies (page 538) to intercept and verify outgoing method calls

     
  9. 优化测试执行和可维护性

    • 使测试运行得更快

     

    • 使测试易于理解和维护

     

    • 设计 SUT 以实现可测试性

     

    • 降低遗漏错误的风险

     
  10. Optimize test execution and maintainability

    • Make the tests run faster

     

    • Make the tests easy to understand and maintain

     

    • Design the SUT for testability

     

    • Reduce the risk of missed bugs

     

这种需求排序并不意味着我们可能会考虑按照这个顺序来实施任何特定的测试。1相反,它很可能是一个项目团队合理地期望了解测试自动化技术的顺序。

This ordering of needs isn't meant to imply that this is the order in which we might think about implementing any specific test.1 Rather, it is likely to be the order in which a project team might reasonably expect to learn about the techniques of test automation.

让我们更详细地看一下这些要点。

Let's look at each of these points in more detail.

练习快乐路径代码

Exercise the Happy Path Code

要使 SUT 顺利运行,我们必须通过 SUT 的 API 将一个简单的成功测试(请参阅第348页的测试方法)自动化为一个简单的往返测试。要使这个测试通过,我们可以简单地对 SUT 中的某些逻辑进行硬编码,尤其是当它可能会调用其他组件来检索所需信息以做出决策,从而使测试顺利进行时。在运行 SUT 之前,我们需要通过将 SUT 初始化为预测试状态来设置测试装置。只要 SUT 执行时没有引发任何错误,我们就认为测试通过;在这个成熟度级别,我们不会检查实际结果与预期结果。

To run the happy path through the SUT, we must automate one Simple Success Test (see Test Method on page 348) as a simple round-trip test through the SUT's API. To get this test to pass, we might simply hard-code some of the logic in the SUT, especially where it might call other components to retrieve information it needs to make decisions that would drive the test down the happy path. Before exercising the SUT, we need to set up the test fixture by initializing the SUT to the pre-test state. As long as the SUT executes without raising any errors, we consider the test as having passed; at this level of maturity we don't check the actual results against the expected results.

验证快乐路径的直接输出

Verify Direct Outputs of the Happy Path

一旦成功执行了快乐路径,我们就可以添加结果验证逻辑,将我们的测试转变为自检测试(参见第 26页)。这涉及添加对断言方法的调用,以将预期结果与实际发生的结果进行比较。我们可以轻松地对 SUT 返回给测试的任何对象或值进行此更改(例如,“返回值”、“输出参数”)。我们还可以在 SUT 上调用其他方法或使用公共字段来访问 SUT 的测试后状态;然后我们也可以对这些值调用断言方法。

Once the happy path is executing successfully, we can add result verification logic to turn our test into a Self-Checking Test (see page 26). This involves adding calls to Assertion Methods to compare the expected results with what actually occurred. We can easily make this change for any objects or values returned to the test by the SUT (e.g., "return values," "out parameters"). We can also call other methods on the SUT or use public fields to access the post-test state of the SUT; we can then call Assertion Methods on these values as well.

验证替代路径

Verify Alternative Paths

此时,代码中的正确路径已经经过了相当好的测试。代码中的替代路径仍为未测试代码(请参阅第268页的生产错误),因此下一步是为这些路径编写测试(无论我们已经编写了生产代码,还是我们正在努力实现自动化测试以促使我们实现它们)。这里要问的问题是“是什么原因导致执行替代路径?”最常见的原因如下:

At this point the happy path through the code is reasonably well tested. The alternative paths through the code are still Untested Code (see Production Bugs on page 268) so the next step is to write tests for these paths (whether we have already written the production code or we are striving to automate the tests that would drive us to implement them). The question to ask here is "What causes the alternative paths to be exercised?" The most common causes are as follows:

  • 客户端作为参数传递的不同值
  • Different values passed in by the client as arguments
  • SUT 本身的先前状态不同
  • Different prior state of the SUT itself
  • 调用 SUT 所依赖的组件的方法会产生不同的结果
  • Different results of invoking methods on components on which the SUT depends

第一种情况可以通过改变测试中的逻辑来测试,这些逻辑调用我们正在执行的 SUT 方法并传递不同的值作为参数。第二种情况涉及使用不同的起始状态初始化 SUT。这两种情况都不需要任何“火箭科学”。然而,第三种情况才是事情变得有趣的地方。

The first case can be tested by varying the logic in our tests that calls the SUT methods we are exercising and passing in different values as arguments. The second case involves initializing the SUT with a different starting state. Neither of these cases requires any "rocket science." The third case, however, is where things get interesting.

控制间接投入

因为其他组件的响应应该导致 SUT 通过代码执行替代路径,所以我们需要控制这些间接输入。我们可以通过使用测试桩来实现这一点,该测试桩返回应将 SUT 驱动到所需代码路径的值。作为夹具设置的一部分,我们必须强制 SUT 使用桩而不是真实组件。测试桩可以通过两种方式构建:作为硬编码测试桩(请参阅测试桩其中包含返回特定值的手写代码,或作为可配置测试桩(请参阅测试桩由测试配置为返回所需的值。在这两种情况下,SUT 都必须使用测试桩而不是真实组件。

Because the responses from other components are supposed to cause the SUT to exercise the alternative paths through the code, we need to get control over these indirect inputs. We can do so by using a Test Stub that returns the value that should drive the SUT into the desired code path. As part of fixture setup, we must force the SUT to use the stub instead of the real component. The Test Stub can be built two ways: as a Hard-Coded Test Stub (see Test Stub), which contains hand-written code that returns the specific values, or as a Configurable Test Stub (see Test Stub), which is configured by the test to return the desired values. In both cases, the SUT must use the Test Stub instead of the real component.

许多替代路径都会导致 SUT 输出“成功”;这些测试被视为简单成功测试,并使用一种称为响应器的测试桩样式(请参阅测试桩)。其他路径预计会引发错误或异常;它们被视为预期异常测试(请参阅测试方法),并使用一种称为破坏者的桩样式(请参阅测试桩)。

Many of these alternative paths result in "successful" outputs from the SUT; these tests are considered Simple Success Tests and use a style of Test Stub called a Responder (see Test Stub). Other paths are expected to raise errors or exceptions; they are considered Expected Exception Tests (see Test Method) and use a style of stub called a Saboteur (see Test Stub).

使测试可重复且可靠

用测试桩替换实际依赖组件 (DOC) 的行为具有非常理想的副作用:它使我们的测试更加健壮且更具可重复性。2通过使用测试桩,我们将可能不确定的组件替换为完全确定且受测试控制的组件。这是隔离 SUT原则的一个很好的例子(参见第43页)。

The act of replacing a real depended-on component (DOC) with a Test Stub has a very desirable side effect: It makes our tests both more robust and more repeatable.2 By using a Test Stub, we replace a possibly nondeterministic component with one that is completely deterministic and under test control. This is a good example of the Isolate the SUT principle (see page 43).

验证间接输出行为

Verify Indirect Output Behavior

到目前为止,我们一直专注于通过检查 SUT 的后状态测试来控制 SUT 的间接输入并验证可见的直接输出。这种结果验证称为状态验证(第 462页)。然而,有时我们无法仅通过查看后测试状态来确认 SUT 是否表现正确。也就是说,我们可能仍有一些未经测试的需求(请参阅生产错误),只能通过行为验证(第 468页) 来验证。

Thus far we have focused on getting control of the indirect inputs of the SUT and verifying readily visible direct outputs by inspecting the post-state test of the SUT. This kind of result verification is known as State Verification (page 462). Sometimes, however, we cannot confirm that the SUT has behaved correctly simply by looking at the post-test state. That is, we may still have some Untested Requirements (see Production Bugs) that can only be verified by doing Behavior Verification (page 468).

我们可以利用测试桩的近亲之一来拦截来自 SUT 的传出方法调用,以此为基础进行构建。测试间谍会“记住”它是如何被调用的,以便测试稍后可以检索使用信息并使用断言方法调用将其与预期使用情况进行比较。可以在夹具设置期间加载模拟对象,并根据预期情况进行加载,随后将其与 SUT 执行过程中发生的实际调用进行比较。

We can build on what we already know how to do by using one of the close relatives of the Test Stub to intercept the outgoing method calls from our SUT. A Test Spy "remembers" how it was called so that the test can later retrieve the usage information and use Assertion Method calls to compare it to the expected usage. A Mock Object can be loaded with expectations during fixture setup, which it subsequently compares with the actual calls as they occur while the SUT is being exercised.

优化测试执行和维护

Optimize Test Execution and Maintenance

至此,我们应该对代码中的所有路径进行自动化测试。然而,我们的测试可能不够理想:

At this point we should have automated tests for all the paths through our code. We may, however, have less than optimal tests:

让测试运行得更快

测试速度慢通常是我们需要解决的第一个行为异味。为了使测试运行得更快,我们可以在多个测试中重用测试装置——例如,通过使用某种形式的共享装置第 317)。不幸的是,这种策略通常会产生自身的问题。用功能等效但执行速度更快的伪对象第 551)替换 DOC 几乎总是更好的解决方案。伪对象的使用建立在我们学到的验证间接输入和输出的技术之上。

Slow Tests is often the first behavior smell we need to address. To make tests run faster, we can reuse the test fixture across many tests—for example, by using some form of Shared Fixture (page 317). Unfortunately, this tactic typically produces its own share of problems. Replacing a DOC with a Fake Object (page 551) that is functionally equivalent but executes much faster is almost always a better solution. Use of a Fake Object builds on the techniques we learned for verifying indirect inputs and outputs.

使测试易于理解和维护

我们可以通过重构测试方法来调用包含常用逻辑的测试实用方法,而不是以内联方式执行所有操作,从而使模糊测试更易于理解,并消除大量测试代码重复。创建方法第 415页)、自定义断言第 474页)、查找器方法(请参阅测试实用方法参数化测试(第607页)都是这种方法的示例。

We can make Obscure Tests easier to understand and remove a lot of Test Code Duplication by refactoring our Test Methods to call Test Utility Methods that contain any frequently used logic instead of doing everything on an in-line basis. Creation Methods (page 415), Custom Assertions (page 474), Finder Methods (see Test Utility Method), and Parameterized Tests (page 607) are all examples of this approach.

如果我们的测试用例类(第 373页) 变得太大而难以理解,我们可以围绕装置或功能重新组织这些类。我们还可以通过系统地命名测试用例类测试方法来更好地传达我们的意图,这些方法公开了我们在其中验证的测试条件。

If our Testcase Classes (page 373) are getting too big to understand, we can reorganize these classes around fixtures or features. We can also better communicate our intent by using a systematic way of naming Testcase Classes and Test Methods that exposes the test conditions we are verifying in them.

降低遗漏错误的风险

如果我们遇到了Bug 测试生产错误问题,我们可以通过封装复杂的测试逻辑来降低误判风险(本不应该通过的测试却通过了)。在这样做时,我们应该为测试实用程序方法使用能够揭示意图的名称。我们应该使用测试实用程序测试来验证非平凡测试实用程序方法的行为(请参阅测试实用程序方法)。

If we are having problems with Buggy Tests or Production Bugs, we can reduce the risk of false negatives (tests that pass when they shouldn't) by encapsulating complex test logic. When doing so, we should use intent-revealing names for our Test Utility Methods. We should verify the behavior of nontrivial Test Utility Methods using Test Utility Tests (see Test Utility Method).

下一步是什么?

What's Next?

本章总结了第一部分叙述”。第 1至14概述了编写有效自动化测试的目标、原则、哲学、模式、异味和编码习惯用法。第二部分“测试异味”第三部分模式”包含这些叙述章节中介绍的每种异味和模式的详细描述,并附有代码示例。

This chapter concludes Part I, The Narratives. Chapters 114 have provided an overview of the goals, principles, philosophies, patterns, smells, and coding idioms related to writing effective automated tests. Part II, The Test Smells, and Part III, The Patterns, contain detailed descriptions of each of the smells and patterns introduced in these narrative chapters, complete with code samples.

第二部分

测试的味道

Part II

The Test Smells

 

第 15 章

代码异味

Chapter 15

Code Smells

 

本章中的气味

Smells in This Chapter

      

模糊测试 186

      

Obscure Test 186

      

条件测试逻辑 200

      

Conditional Test Logic 200

      

难以测试的代码 209

      

Hard-to-Test Code 209

      

测试代码重复 213

      

Test Code Duplication 213

      

生产中的测试逻辑 217

      

Test Logic in Production 217

模糊测试

Obscure Test

也称为

Also known as

长测试、复杂测试、详细测试

Long Test, Complex Test, Verbose Test

乍一看很难理解这个测试。

It is difficult to understand the test at a glance.

自动化测试至少应有两个目的。首先,它们应作为被测系统 (SUT)如何运行的文档;我们称之为“测试即文档”(参见第23页)。其次,它们应是可自我验证的可执行规范。这两个目标通常是相互矛盾的,因为测试可执行所需的详细程度可能会使测试过于冗长而难以理解。

Automated tests should serve at least two purposes. First, they should act as documentation of how the system under test (SUT) should behave; we call this Tests as Documentation (see page 23). Second, they should be a self-verifying executable specification. These two goals are often contradictory because the level of detail needed for tests to be executable may make the test so verbose as to be difficult to understand.

症状

Symptoms

我们很难理解测试正在验证什么行为。

We are having trouble understanding what behavior a test is verifying.

影响

Impact

模糊测试的第一个问题是它使测试更难理解,因此也更难维护。它几乎肯定会妨碍实现测试作为文档,从而导致高昂的测试维护成本(第265页)。

The first issue with an Obscure Test is that it makes the test harder to understand and therefore maintain. It will almost certainly preclude achieving Tests as Documentation, which in turn can lead to High Test Maintenance Cost (page 265).

模糊测试的第二个问题是,由于模糊测试中隐藏的测试编码错误,错误可能会漏掉。这可能会导致错误测试第 260页)。此外,急切测试中一个断言的失败可能会隐藏更多未运行的错误,从而导致测试调试数据丢失。

The second issue with an Obscure Test is that it may allow bugs to slip through because of test coding errors hidden in the Obscure Test. This can result in Buggy Tests (page 260). Furthermore, a failure of one assertion in an Eager Test may hide many more errors that simply aren't run, leading to a loss of test debugging data.

原因

Causes

矛盾的是,模糊测试可能是由于测试方法(第 348页)中的信息过多或信息过少造成的。神秘嘉宾是信息过少的一个例子;急切测试无关信息是信息过多的例子。

Paradoxically, an Obscure Test can be caused by either too much information in the Test Method (page 348) or too little information. Mystery Guest is an example of too little information; Eager Test and Irrelevant Information are examples of too much information.

模糊测试的根本原因通常是缺乏对保持测试代码简洁和简单的关注。测试代码与生产代码同样重要,并且需要同样频繁地重构。模糊测试的主要原因是编写测试时“只需在线执行”的心态。将代码放在在线中会导致测试方法庞大而复杂,因为有些事情需要大量代码才能完成。

The root cause of an Obscure Test is typically a lack of attention to keeping the test code clean and simple. Test code is just as important as the production code, and it needs to be refactored just as often. A major contributor to an Obscure Test is a "just do it in-line" mentality when writing tests. Putting code in-line results in large, complex Test Methods because some things just take a lot of code to do.

这里讨论的模糊测试的几个原因与测试中的错误信息有关:

The first few causes of Obscure Test discussed here relate to having the wrong information in the test:

  • 急切测试:测试在单个测试方法中验证了太多功能。
  • Eager Test: The test verifies too much functionality in a single Test Method.
  • 神秘嘉宾:测试阅读器无法看到夹具和验证逻辑之间的因果关系,因为其中一部分是在测试方法之外完成的。
  • Mystery Guest: The test reader is not able to see the cause and effect between fixture and verification logic because part of it is done outside the Test Method.

详细测试的一般问题(测试使用了太多代码来表达需要表达的内容)可以进一步分解为多个根本原因:

The general problem of Verbose Tests—tests that use too much code to say what they need to say—can be further broken down into a number of root causes:

  • 通用夹具:测试构建或引用比验证相关功能所需更大的夹具。
  • General Fixture: The test builds or references a larger fixture than is needed to verify the functionality in question.
  • 不相关的信息:测试暴露了许多与装置有关的不相关的细节,这些细节分散了测试读者的注意力,使他们无法关注真正影响 SUT 行为的因素。
  • Irrelevant Information: The test exposes a lot of irrelevant details about the fixture that distract the test reader from what really affects the behavior of the SUT.
  • 硬编码测试数据: SUT 的夹具、断言或参数中的数据值在测试方法中是硬编码的,从而掩盖了输入和预期输出之间的因果关系。
  • Hard-Coded Test Data: Data values in the fixture, assertions, or arguments of the SUT are hard-coded in the Test Method, obscuring cause–effect relationships between inputs and expected outputs.
  • 间接测试:测试方法通过另一个对象间接与 SUT 交互,从而使交互更加复杂。
  • Indirect Testing: The Test Method interacts with the SUT indirectly via another object, thereby making the interactions more complex.
原因:急切测试

该测试在单个测试方法中验证了太多功能。

The test verifies too much functionality in a single Test Method.

症状

测试不断地验证这个、那个,以及“除了厨房水槽之外的所有东西”。很难分辨哪个部分是固定装置设置,哪个部分是在锻炼 SUT。

The test goes on and on verifying this, that, and "everything but the kitchen sink." It is hard to tell which part is fixture setup and which part is exercising the SUT.

public void testFlightMileage_asKm2() throws Exception {

      // 设置夹具

      // 练习构造函数

      Flight newFlight = new Flight(validFlightNumber);

      // 验证构造的对象

      assertEquals(validFlightNumber, newFlight.number);

      assertEquals("", newFlight.airlineCode);

      assertNull(newFlight.airline);

      // 设置里程

      newFlight.setMileage(1122);

      // 练习里程转换器

      int actualKilometres = newFlight.getMileageAsKm();

      // 验证结果

      int expectedKilometres = 1810;

      assertEquals( expectedKilometres, actualKilometres);

      // 现在用取消的航班尝试

      newFlight.cancel();

      try {

          newFlight.getMileageAsKm();

          fail("Expected exception");

      } catch (InvalidRequestException e) {

          assertEquals( "无法获取取消的航班里程",

                              e.getMessage());

      }

}

public  void  testFlightMileage_asKm2()  throws  Exception  {

      //  set  up  fixture

      //  exercise  constructor

      Flight  newFlight  =  new  Flight(validFlightNumber);

      //  verify  constructed  object

      assertEquals(validFlightNumber,  newFlight.number);

      assertEquals("",  newFlight.airlineCode);

      assertNull(newFlight.airline);

      //  set  up  mileage

      newFlight.setMileage(1122);

      //  exercise  mileage  translator

      int  actualKilometres  =  newFlight.getMileageAsKm();

      //  verify  results

      int  expectedKilometres  =  1810;

      assertEquals(  expectedKilometres,  actualKilometres);

      //  now  try  it  with  a  canceled  flight

      newFlight.cancel();

      try  {

          newFlight.getMileageAsKm();

          fail("Expected  exception");

      }  catch  (InvalidRequestException  e)  {

          assertEquals(  "Cannot  get  cancelled  flight  mileage",

                              e.getMessage());

      }

}

 
根本原因

手动执行测试时,将多个逻辑上不同的测试条件链接到单个测试用例中是有意义的,以减少每个测试的设置开销。这样做是因为我们有一个实时软件(一个智能人)执行测试,这个人可以随时决定是否继续进行,或者某个步骤的失败是否严重到需要放弃测试的执行。

When executing tests manually, it makes sense to chain a number of logically distinct test conditions into a single test case to reduce the setup overhead of each test. This works because we have liveware (an intelligent human being) executing the tests, and this person can decide at any point whether it makes sense to keep going or whether the failure of a step is severe enough to abandon the execution of the test.

可能的解决方案

当测试自动化时,最好有一套独立的单一条件测试(参见第45页),因为它们可以提供更好的缺陷定位(参见第22页)。

When the tests are automated, it is better to have a suite of independent Single-Condition Tests (see page 45) as these provide much better Defect Localization (see page 22).

原因:神秘嘉宾

测试读取者无法看到夹具和验证逻辑之间的因果关系,因为其中一部分是在测试方法之外完成的。

The test reader is not able to see the cause and effect between fixture and verification logic because part of it is done outside the Test Method.

症状

测试总是需要将数据传递给 SUT。四阶段测试(第 358页) 中的夹具设置和练习 SUT 阶段使用的数据定义了 SUT 的先决条件并影响其行为方式。后置条件(预期结果)反映在测试验证结果阶段作为参数传递给断言方法(第 362页) 的数据中。

Tests invariably require passing data to the SUT. The data used in the fixture setup and exercise SUT phases of the Four-Phase Test (page 358) define the pre-conditions of the SUT and influence how it should behave. The post-conditions (the expected outcomes) are reflected in the data passed as arguments to the Assertion Methods (page 362) in the verify outcome phase of the test.

当测试的夹具设置或结果验证部分依赖于测试中不可见的信息,并且测试阅读者发现很难在不先找到并检查外部信息的情况下理解正在验证的行为时,我们就有了一个神秘的客人。以下是一个例子,我们无法分辨夹具是什么样子的,这使得很难将预期结果与测试的先决条件联系起来:

When either the fixture setup or the result verification part of a test depends on information that is not visible within the test and the test reader finds it difficult to understand the behavior that is being verified without first finding and inspecting the external information, we have a Mystery Guest on our hands. Here's an example where we cannot tell what the fixture looks like, making it difficult to relate the expected outcome to the pre-conditions of the test:

public void testGetFlightsByFromAirport_OneOutboundFlight_mg()

                      throws Exception {

      loadAirportsAndFlightsFromFile("test-flights.csv");

      // 练习系统

      列表 flightsAtOrigin = Facade.getFlightsByOriginAirportCode( "YYC");

      // 验证结果

      assertEquals( 1, flightsAtOrigin.size());

      FlightDto firstFlight = (FlightDto) flightsAtOrigin.get(0);

      assertEquals( "Calgary", firstFlight.getOriginCity());

}

public  void  testGetFlightsByFromAirport_OneOutboundFlight_mg()

                      throws  Exception  {

      loadAirportsAndFlightsFromFile("test-flights.csv");

      //  Exercise  System

      List  flightsAtOrigin  =  facade.getFlightsByOriginAirportCode(  "YYC");

      //  Verify  Outcome

      assertEquals(  1,  flightsAtOrigin.size());

      FlightDto  firstFlight  =  (FlightDto)  flightsAtOrigin.get(0);

      assertEquals(  "Calgary",  firstFlight.getOriginCity());

}

 
影响

神秘嘉宾使得很难看出测试装置(测试的先决条件)与测试预期结果之间的因果关系。因此,测试无法履行“测试即文档”的角色。更糟糕的是,有人可能会修改或删除外部资源,而没有意识到此操作在测试运行时会产生的影响。这种行为异味有自己的名字:资源乐观主义(请参阅第228页的“不稳定的测试”)!

The Mystery Guest makes it hard to see the cause–effect relationship between the test fixture (the pre-conditions of the test) and the expected outcome of the test. As a consequence, the tests don't fulfill the role of Tests as Documentation. Even worse, someone may modify or delete the external resource without realizing the impact this action will have when the tests are run. This behavior smell has its own name: Resource Optimism (see Erratic Test on page 228)!

如果神秘嘉宾是一个共享装置(第 317页),其他测试修改它时也可能会导致不稳定测试。

If the Mystery Guest is a Shared Fixture (page 317), it may also lead to Erratic Tests if other tests modify it.

根本原因

测试依赖于神秘的外部资源,因此很难理解它所验证的行为。神秘嘉宾可能有多种形式:

A test depends on mysterious external resources, making it difficult to understand the behavior that it is verifying. Mystery Guests may take many forms:

  • 现有外部文件的文件名被传递给 SUT 的方法;文件的内容应该决定 SUT 的行为。
  • A filename of an existing external file is passed to a method of the SUT; the contents of the file should determine the behavior of the SUT.
  • 由文字键标识的数据库记录的内容被读入对象,然后由测试使用或传递给 SUT。
  • The contents of a database record identified by a literal key are read into an object that is then used by the test or passed to the SUT.
  • 读取文件的内容并将其用于调用断言方法来验证预期的结果。
  • The contents of a file are read and used in calls to Assertion Methods to verify the expected outcome.
  • 设置装饰器(第447页) 用于创建共享装置,然后通过结果验证逻辑中的变量引用此装置中的对象。
  • A Setup Decorator (page 447) is used to create a Shared Fixture, and objects in this fixture are then referenced via variables within the result verification logic.
  • 使用隐式设置第 424页)设置通用夹具,然后测试方法通过实例变量或类变量访问它们。
  • A General Fixture is set up using Implicit Setup (page 424), and the Test Methods then access them via instance variables or class variables.

所有这些场景都有一个共同的结果:很难看出测试装置与测试预期结果之间的因果关系,因为相关数据在测试中不可见。如果我们给变量和包含它们的文件起的名字没有清楚地描述数据的内容,我们就会遇到神秘客人

All of these scenarios share a common outcome: It is hard to see the cause–effect relationship between the test fixture and the expected outcome of the test because the relevant data are not visible in the tests. If the contents of the data are not clearly described by the names we give to the variables and files that contain them, we have a Mystery Guest.

可能的解决方案

对于神秘客人 (Mystery Guest) ,使用通过内联设置 (In-line Setup) (第 408页)构建的Fresh Fixture (第311页) 显然是解决方案。应用于文件示例时,这将涉及在测试中将文件内容创建为字符串以使内容可见,然后将其写出到文件系统 [设置外部资源 (第772页) 重构] 或将其放入文件系统测试桩(第529页) 作为 Fixture 设置的一部分。1为了避免无关信息 (Irrelevant Information ),我们可能希望将构造细节隐藏在一个或多个命名富有启发性的创建方法(第415页) 后面,这些方法会附加到文件的内容中。

Using a Fresh Fixture (page 311) built using In-line Setup (page 408) is the obvious solution for a Mystery Guest. When applied to the file example, this would involve creating the contents of the file as a string within our test so that the contents are visible and then writing them out to the file system [Setup External Resource (page 772) refactoring] or putting it into a file system Test Stub (page 529) as part of the fixture setup.1 To avoid Irrelevant Information, we may want to hide the details of the construction behind one or more evocatively named Creation Methods (page 415) that append to the file's contents.

如果必须使用共享装置隐式设置,则应考虑使用命名醒目的Finder 方法(请参阅第599页的测试实用程序方法)来访问装置中的对象。如果必须使用文件等外部资源,则应将它们放入特殊文件夹或目录中,并为其命名,使其清楚表明其中保存了何种数据。

If we must use a Shared Fixture or Implicit Setup, we should consider using evocatively named Finder Methods (see Test Utility Method on page 599) to access the objects in the fixture. If we must use external resources such as files, we should put them into a special folder or directory and give them names that make it obvious what kind of data they hold.

原因:通用装置

测试构建或引用的装置比验证相关功能所需的装置更大。

The test builds or references a larger fixture than is needed to verify the functionality in question.

症状

似乎构建了大量的测试装置 — 远远超过任何特定测试所需的数量。很难理解装置、被测试系统 (SUT) 被执行的部分以及测试的预期结果之间的因果关系。

There seems to be a lot of test fixture being built—much more than would appear to be necessary for any particular test. It is hard to understand the cause–effect relationship between the fixture, the part of the SUT being exercised, and the expected outcome of a test.

考虑以下一组测试:

Consider the following set of tests:

public void testGetFlightsByFromAirport_OneOutboundFlight()

                  throws Exception {

     setupStandardAirportsAndFlights();

     FlightDto outboundFlight = findOneOutboundFlight();

     // 练习系统

     列表 flightsAtOrigin = Facade.getFlightsByOriginAirport(

                           outboundFlight.getOriginAirportId());

    // 验证结果

    assertOnly1FlightInDtoList( "Flights at origin",

                                            outboundFlight,

                                            flightsAtOrigin);

}



public void testGetFlightsByFromAirport_TwoOutboundFlights()

                 throws Exception {

      setupStandardAirportsAndFlights();

      FlightDto[] outboundFlights =

                      findTwoOutboundFlightsFromOneAirport();

      // 练习系统

      列表 flightsAtOrigin = Facade.getFlightsByOriginAirport(

                           outboundFlights[0].getOriginAirportId());

      // 验证结果

      assertExactly2FlightsInDtoList( "出发地航班",

                                                   outboundFlights,

                                                   flightsAtOrigin);

}

public  void  testGetFlightsByFromAirport_OneOutboundFlight()

                  throws  Exception  {

     setupStandardAirportsAndFlights();

     FlightDto  outboundFlight  =  findOneOutboundFlight();

     //  Exercise  System

     List  flightsAtOrigin  =  facade.getFlightsByOriginAirport(

                           outboundFlight.getOriginAirportId());

    //  Verify  Outcome

    assertOnly1FlightInDtoList(  "Flights  at  origin",

                                            outboundFlight,

                                            flightsAtOrigin);

}



public  void  testGetFlightsByFromAirport_TwoOutboundFlights()

                 throws  Exception  {

      setupStandardAirportsAndFlights();

      FlightDto[]  outboundFlights  =

                      findTwoOutboundFlightsFromOneAirport();

      //  Exercise  System

      List  flightsAtOrigin  =  facade.getFlightsByOriginAirport(

                           outboundFlights[0].getOriginAirportId());

      //  Verify  Outcome

      assertExactly2FlightsInDtoList(  "Flights  at  origin",

                                                   outboundFlights,

                                                   flightsAtOrigin);

}

 

通过阅读练习 SUT 并验证测试的结果部分,似乎它们需要非常不同的装置。即使这些测试使用的是Fresh Fixture设置策略,它们也通过调用方法使用相同的装置设置逻辑。该方法的名称是这个经典但易于识别的General FixturesetupStandardAirportsAndFlights示例的线索。更难诊断的情况是,如果每个测试都在线创建了Standard Fixture (第 305页),或者如果每个测试都创建了一个略有不同的装置,但每个装置包含的内容远远超过每个单独测试所需的内容。

From reading the exercise SUT and verifing outcome parts of the tests, it would appear that they need very different fixtures. Even though these tests are using a Fresh Fixture setup strategy, they are using the same fixture setup logic by calling the setupStandardAirportsAndFlights method. The name of the method is a clue to this classic but easily recognized example of a General Fixture. A more difficult case to diagnose would be if each test created the Standard Fixture (page 305) in-line or if each test created a somewhat different fixture but each fixture contained much more than was needed by each individual test.

我们还可能会遇到“慢速测试”第 253页)或“易碎固定装置”(请参阅​​第 239页的“易碎测试”)。

We may also be experiencing Slow Tests (page 253) or a Fragile Fixture (see Fragile Test on page 239).

根本原因

导致此问题的最常见原因是测试使用了旨在支持许多测试的装置。示例包括在许多具有不同装置要求的测试中使用隐式设置共享装置。此问题导致装置变得庞大且难以理解。装置还可能随着时间的推移而变大。根本原因是这两种方法都依赖于标准装置,该装置必须满足使用它的所有测试的要求。这些测试的需求越多样化,我们就越有可能创建通用装置

The most common cause of this problem is a test that uses a fixture that is designed to support many tests. Examples include the use of Implicit Setup or a Shared Fixture across many tests with different fixture requirements. This problem results in the fixture becoming large and difficult to understand. The fixture may also grow larger over time. The root cause is that both approaches rely on a Standard Fixture that must meet the requirements of all tests that use it. The more diverse the needs of those tests, the more likely we are to create a General Fixture.

影响

当测试装置被设计为支持许多不同的测试时,理解每个测试如何使用装置会非常困难。这种复杂性降低了将测试用作文档的可能性,并且可能导致装置变得脆弱,因为人们会修改装置以使其能够处理新测试。它还可能导致测试速度变慢, 因为较大的装置需要更多时间来构建,尤其是在涉及文件系统或数据库的情况下。

When the test fixture is designed to support many different tests, it can be very difficult to understand how each test uses the fixture. This complexity reduces the likelihood of using Tests as Documentation and can result in a Fragile Fixture as people alter the fixture so that it can handle new tests. It can also result in Slow Tests because a larger fixture takes more time to build, especially if a file system or database is involved.

可能的解决方案

我们需要转向Minimal Fixture第 302页)来解决这个问题。为此,我们可以为每个测试使用一个Fresh Fixture 。如果必须使用Shared Fixture,我们应该考虑应用Make Resource Unique第 737页)重构来为每个测试创建一个虚拟Database Sandbox(第650页)。2

We need to move to a Minimal Fixture (page 302) to address this problem. To do so, we can use a Fresh Fixture for each test. If we must use a Shared Fixture, we should consider applying the Make Resource Unique (page 737) refactoring to create a virtual Database Sandbox (page 650) for each test.2

原因:不相关的信息

测试暴露了许多与装置有关的不相关的细节,分散了测试读者对真正影响 SUT 行为的注意力。

The test exposes a lot of irrelevant details about the fixture that distract the test reader from what really affects the behavior of the SUT.

症状

作为测试读者,我们发现很难确定传递给对象的哪些值实际上会影响预期结果:

As test readers, we find it hard to determine which of the values passed to objects actually affect the expected outcome:

public void testAddItemQuantity_severalQuantity_v10(){

      //设置设备

      地址 billingAddress =

           createAddress( "1222 1st St SW", "Calgary", "Alberta",

                                  "T2N 2V2", "Canada");

      地址 shippingAddress =

           createAddress( "1333 1st St SW", "Calgary", "Alberta",

                                  "T2N 2V2", "Canada");

      客户 customer =

           createCustomer( 99, "John", "Doe", new BigDecimal("30"),

                                       billingAddress, shippingAddress);

      产品 product =

           createProduct( 88,"SomeWidget",new BigDecimal("19.99"));

      发票 invoice = createInvoice(customer);

      //练习 SUT

      invoice.addItemQuantity(product, 5);

      // 验证结果

      LineItem expected =

            new LineItem(invoice, product,5, new BigDecimal("30"),

                               new BigDecimal("69.96"));

      assertContainsExactlyOneLineItem(invoice, expected);

}

public  void  testAddItemQuantity_severalQuantity_v10(){

      //    Set  Up  Fixture

      Address  billingAddress  =

           createAddress(  "1222  1st  St  SW",  "Calgary",  "Alberta",

                                  "T2N  2V2",  "Canada");

      Address  shippingAddress  =

           createAddress(  "1333  1st  St  SW",  "Calgary",  "Alberta",

                                  "T2N  2V2",  "Canada");

      Customer  customer  =

           createCustomer(  99,  "John",  "Doe",  new  BigDecimal("30"),

                                       billingAddress,  shippingAddress);

      Product  product  =

           createProduct(  88,"SomeWidget",new  BigDecimal("19.99"));

      Invoice  invoice  =  createInvoice(customer);

      //  Exercise  SUT

      invoice.addItemQuantity(product,  5);

      //  Verify  Outcome

      LineItem  expected  =

            new  LineItem(invoice,  product,5,  new  BigDecimal("30"),

                               new  BigDecimal("69.96"));

      assertContainsExactlyOneLineItem(invoice,  expected);

}

 

夹具设置逻辑可能看起来很长且复杂,因为它将许多相互关联的对象编织在一起。这使得很难确定测试正在验证什么,因为读者不了解测试的先决条件:

Fixture setup logic may seem very long and complicated as it weaves together many interrelated objects. This makes it hard to determine what the test is verifying because the reader doesn't understand the pre-conditions of the test:

public void testGetFlightsByOriginAirport_TwoOutboundFlights()

         抛出异常 {

     FlightDto expectedCalgaryToSanFran = new FlightDto();

     expectedCalgaryToSanFran.setOriginAirportId(calgaryAirportId);

     expectedCalgaryToSanFran.setOriginCity(CALGARY_CITY);

     expectedCalgaryToSanFran.setDestinationAirportId(sanFranAirportId);

     expectedCalgaryToSanFran.setDestinationCity(SAN_FRAN_CITY);

     expectedCalgaryToSanFran.setFlightNumber(

          facade.createFlight(calgaryAirportId,sanFranAirportId));

      FlightDto expectedCalgaryToVan = new FlightDto();

      expectedCalgaryToVan.setOriginAirportId(calgaryAirportId);

      expectedCalgaryToVan.setOriginCity(CALGARY_CITY);

      expectedCalgaryToVan.setDestinationAirportId

                (vancouverAirportId);

      expectedCalgaryToVan.setDestinationCity(VANCOUVER_CITY);

      expectedCalgaryToVan.setFlightNumber(facade.createFlight(

                calgaryAirportId, vancouverAirportId));

public  void  testGetFlightsByOriginAirport_TwoOutboundFlights()

         throws  Exception  {

     FlightDto  expectedCalgaryToSanFran  =  new  FlightDto();

     expectedCalgaryToSanFran.setOriginAirportId(calgaryAirportId);

     expectedCalgaryToSanFran.setOriginCity(CALGARY_CITY);

     expectedCalgaryToSanFran.setDestinationAirportId(sanFranAirportId);

     expectedCalgaryToSanFran.setDestinationCity(SAN_FRAN_CITY);

     expectedCalgaryToSanFran.setFlightNumber(

          facade.createFlight(calgaryAirportId,sanFranAirportId));

      FlightDto  expectedCalgaryToVan  =  new  FlightDto();

      expectedCalgaryToVan.setOriginAirportId(calgaryAirportId);

      expectedCalgaryToVan.setOriginCity(CALGARY_CITY);

      expectedCalgaryToVan.

                setDestinationAirportId(vancouverAirportId);

      expectedCalgaryToVan.setDestinationCity(VANCOUVER_CITY);

      expectedCalgaryToVan.setFlightNumber(facade.createFlight(

                calgaryAirportId,  vancouverAirportId));

 

验证测试预期结果的代码也可能太复杂而难以理解:

The code that verifies the expected outcome of a test can also be too complicated to understand:

      列表 lineItems = inv.getLineItems();

      assertEquals("项目数量", lineItems.size(), 2);

      // 验证第一项

      LineItem actual = (LineItem)lineItems.get(0);

      assertEquals(expItem1.getInv(), actual.getInv());

      assertEquals(expItem1.getProd(), actual.getProd());

      assertEquals(expItem1.getQuantity(), actual.getQuantity());

      // 验证第二项

      actual = (LineItem)lineItems.get(1);

      assertEquals(expItem2.getInv(), actual.getInv());

      assertEquals(expItem2.getProd(), actual.getProd());

      assertEquals(expItem2.getQuantity(), actual.getQuantity());

}

      List  lineItems  =  inv.getLineItems();

      assertEquals("number  of  items",  lineItems.size(),  2);

      //    verify  first  item

      LineItem  actual  =  (LineItem)lineItems.get(0);

      assertEquals(expItem1.getInv(),  actual.getInv());

      assertEquals(expItem1.getProd(),  actual.getProd());

      assertEquals(expItem1.getQuantity(),  actual.getQuantity());

      //    verify  second  item

      actual  =  (LineItem)lineItems.get(1);

      assertEquals(expItem2.getInv(),  actual.getInv());

      assertEquals(expItem2.getProd(),  actual.getProd());

      assertEquals(expItem2.getQuantity(),  actual.getQuantity());

}

 
根本原因

测试包含大量数据,既可以是文字值第 714页),也可以是变量。无关信息通常与硬编码测试数据通用装置一起出现,但也可能因为我们将测试需要执行的所有数据都公开,而不是专注于测试需要理解的数据而出现。编写测试时,阻力最小的路径是使用任何可用的方法(在 SUT 和其他对象上),并用值填充所有参数,无论它们是否与测试相关。

A test contains a lot of data, either as Literal Values (page 714) or as variables. Irrelevant Information often occurs in conjunction with Hard-Coded Test Data or a General Fixture but can also arise because we make visible all data the test needs to execute rather than focusing on the data the test needs to be understood. When writing tests, the path of least resistance is to use whatever methods are available (on the SUT and other objects) and to fill in all parameters with values, whether or not they are relevant to the test.

另一个可能的原因是,当我们使用程序状态验证(请参阅第 462页的状态验证)包含验证结果所需的所有代码时,而不是使用更紧凑的“声明性”样式来指定预期结果。

Another possible cause is when we include all the code needed to verify the outcome using Procedural State Verification (see State Verification on page 462) rather than using a much more compact "declarative" style to specify the expected outcome.

影响

如果测试包含许多看似随机的模糊测试片段,无法明确将先决条件与后置条件联系起来,则很难实现测试作为文档。同样,费力地完成许多夹具设置步骤或结果验证逻辑可能会导致高昂的测试维护成本,并可能增加生产错误第 268页)或错误测试的可能性。

It is hard to achieve Tests as Documentation if the tests contain many seemingly random bits of Obscure Test that don't clearly link the pre-conditions with the post-conditions. Likewise, wading through many steps of fixture setup or result verification logic can result in High Test Maintenance Cost and can increase the likelihood of Production Bugs (page 268) or Buggy Tests.

可能的解决方案

消除夹具设置逻辑中不相关信息的最佳方法是用参数化创建方法(参见创建方法)调用替换对构造函数或工厂方法 [GOF]的直接调用,这些方法仅将相关信息作为参数。对测试无关的夹具值(即不影响预期结果的值)应在创建方法中默认或用虚拟对象(第 728页)替换。通过这种方式,我们可以告诉测试读者:“您看不到的值不会影响预期结果。”只要我们使用Fresh Fixture方法进行夹具设置,我们就可以用适当初始化的命名常量替换出现在测试的夹具设置和结果验证部分中的夹具值。

The best way to get rid of Irrelevant Information in fixture setup logic is to replace direct calls to the constructor or Factory Methods [GOF] with calls to Parameterized Creation Methods (see Creation Method) that take only the relevant information as parameters. Fixture values that do not matter to the test (i.e., those that do not affect the expected outcome) should be defaulted within Creation Methods or replaced by Dummy Objects (page 728). In this way we say to the test reader, "The values you don't see don't affect the expected outcome." We can replace fixture values that appear in both the fixture setup and outcome verification parts of the test with suitably initialized named constants as long as we are using a Fresh Fixture approach to fixture setup.

为了隐藏结果验证逻辑中的不相关信息,我们可以对整个预期对象使用断言(参见状态验证),而不是对单个字段进行断言,并且我们可以创建自定义断言第 474页)来隐藏复杂的程序验证逻辑。

To hide Irrelevant Information in result verification logic, we can use assertions on entire Expected Objects (see State Verification), rather than asserting on individual fields, and we can create Custom Assertions (page 474) that hide complex procedural verification logic.

原因:硬编码测试数据

被测对象 (SUT) 的装置、断言或参数中的数据值在测试方法中是硬编码的,从而掩盖了输入和预期输出之间的因果关系。

Data values in the fixture, assertions, or arguments of the SUT are hard-coded in the Test Method, obscuring cause–effect relationships between inputs and expected outputs.

症状

作为测试阅读者,我们发现很难确定测试中各种硬编码(即文字)值如何相互关联,以及哪些值应该影响 SUT 的行为。我们还可能遇到行为异味,例如不稳定测试

As test readers, we find it difficult to determine how various hard-coded (i.e., literal) values in the test are related to one another and which values should affect the behavior of the SUT. We may also encounter behavior smells such as Erratic Tests.

public void testAddItemQuantity_severalQuantity_v12(){

    // 设置 Fixture

    Customer cust = createACustomer(new BigDecimal("30"));

    Product prod = createAProduct(new BigDecimal("19.99"));

    Invoice invoice = createInvoice(cust);

    // 练习 SUT

    invoice.addItemQuantity(prod, 5);

    // 验证结果

    LineItem expected = new LineItem(invoice, prod, 5,

            new BigDecimal("30"), new BigDecimal("69.96"));

    assertContainsExactlyOneLineItem(invoice, expected);

}

public  void  testAddItemQuantity_severalQuantity_v12(){

    //    Set  Up  Fixture

    Customer  cust  =  createACustomer(new  BigDecimal("30"));

    Product  prod  =  createAProduct(new  BigDecimal("19.99"));

    Invoice  invoice  =  createInvoice(cust);

    //  Exercise  SUT

    invoice.addItemQuantity(prod,  5);

    //  Verify  Outcome

    LineItem  expected  =  new  LineItem(invoice,  prod,  5,

            new  BigDecimal("30"),  new  BigDecimal("69.96"));

    assertContainsExactlyOneLineItem(invoice,  expected);

}

 

这个具体的例子还不算太糟,因为字面值并不多。但是,如果我们不擅长心算,我们可能会忽略单价(19.99 美元)、商品数量(5)、折扣(30%)和总价(69.96 美元)之间的关系。

This specific example isn't so bad because there aren't very many literal values. If we aren't good at doing math in our heads, however, we might miss the relationship between the unit price ($19.99), the item quantity (5), the discount (30%), and the total price ($69.96).

根本原因

当测试包含大量看似不相关的文字值时,就会出现硬编码测试数据。测试总是需要将数据传递给 SUT。四阶段测试的夹具设置和练习 SUT 阶段中使用的数据定义了 SUT 的先决条件并影响其行为方式。后置条件(预期结果)反映在测试验证结果阶段作为参数传递给断言方法的数据中。编写测试时,阻力最小的路径是使用任何可用的方法(在 SUT 和其他对象上),并用值填充所有参数,无论它们是否与测试相关。

Hard-Coded Test Data occurs when a test contains a lot of seemingly unrelated Literal Values. Tests invariably require passing data to the SUT. The data used in the fixture setup and exercise SUT phases of the Four-Phase Test define the preconditions of the SUT and influence how it should behave. The post-conditions (the expected outcomes) are reflected in the data passed as arguments to the Assertion Methods in the verify outcome phase of the test. When writing tests, the path of least resistance is to use whatever methods are available (on the SUT and other objects) and to fill in all parameters with values, whether or not they are relevant to the test.

当我们使用“剪切粘贴”重用测试逻辑时,我们发现自己将文字值复制到衍生测试中。

When we use "cut-and-paste" reuse of test logic, we find ourselves replicating the literal values to the derivative tests.

影响

如果测试包含许多看似随机的模糊测试片段,无法明确将先决条件与后置条件联系起来,则很难实现测试即文档。一些文字参数可能看起来不是坏事——毕竟,它们不需要我们付出太多努力来理解测试。然而,随着文字值数量的增加,理解测试会变得更加困难。当信噪比急剧下降时尤其如此,因为大多数值与测试无关。

It is hard to achieve Tests as Documentation if the tests contain many seemingly random bits of Obscure Test that don't clearly link the pre-conditions with the post-conditions. A few literal parameters might not seem like a bad thing—after all, they don't require us to make that much more effort to understand a test. As the number of literal values grows, however, it can become much more difficult to understand a test. This is especially true when the signal-to-noise ratio drops dramatically because the majority of the values are irrelevant to the test.

第二个主要影响是测试之间发生碰撞,因为测试使用相同的值。这种情况仅在我们使用共享夹具时才会发生,因为新鲜夹具策略不应该在场景中散落任何可能与后续测试发生碰撞的对象。

The second major impact occurs when collisions between tests occur because the tests are using the same values. This situation happens only when we use a Shared Fixture because a Fresh Fixture strategy shouldn't litter the scene with any objects with which a subsequent test can collide.

可能的解决方案

消除模糊测试味道的最佳方法是用其他东西替换文字常量。确定正在执行哪个场景的 Fixture 值(例如类型代码)可能是唯一可以合理地保留为文字的值 — 但即使这些值也可以转换为命名常量。

The best way to get rid of the Obscure Test smell is to replace the literal constants with something else. Fixture values that determine which scenario is being executed (e.g., type codes) are probably the only ones that are reasonable to leave as literals—but even these values can be converted to named constants.

对测试不重要的 Fixture 值(即不影响预期结果的值)应在Creation Methods中默认。这样,我们就可以告诉测试读者:“您看不到的值不会影响预期结果。”只要我们使用Fresh Fixture方法进行 Fixture 设置,我们就可以用适当初始化的命名常量替换 Fixture 设置和测试结果验证部分中出现的 Fixture 值。

Fixture values that do not matter to the test (i.e., those that do not affect the expected outcome) should be defaulted within Creation Methods. In this way we say to the test reader, "The values you don't see don't affect the expected outcome." We can replace fixture values that appear in both the fixture setup and outcome verification parts of the test with suitably initialized named constants as long as we are using a Fresh Fixture approach to fixture setup.

结果验证逻辑中基于夹具中使用的值或用作 SUT 参数的值应替换为派生值(第718页),以使测试读者能够清楚地看到这些计算。

Values in the result verification logic that are based on values used in the fixture or that are used as arguments of the SUT should be replaced with Derived Values (page 718) to make those calculations obvious to the test reader.

如果我们使用任何Shared Fixture变体,我们应尝试使用不同的生成值(请参阅第723页的生成值)以确保每次运行测试时都使用不同的值。对于在数据库中充当唯一键的字段,此考虑尤其重要。封装此逻辑的常用方法是使用匿名创建方法(请参阅创建方法)。

If we are using any variant of Shared Fixture, we should try to use Distinct Generated Values (see Generated Value on page 723) to ensure that each time a test is run, it uses a different value. This consideration is especially important for fields that serve as unique keys in databases. A common way of encapsulating this logic is to use Anonymous Creation Methods (see Creation Method).

原因:间接测试

测试方法通过另一个对象间接与 SUT 交互,从而使交互更加复杂。

The Test Method interacts with the SUT indirectly via another object, thereby making the interactions more complex.

症状

测试主要与其旨在验证其行为的对象以外的对象进行交互。测试必须构建并交互包含对 SUT 的引用的对象,而不是与 SUT 本身进行交互。通过表示层测试业务逻辑是间接测试的一个常见示例。

A test interacts primarily with objects other than the one whose behavior it purports to verify. The test must construct and interact with objects that contain references to the SUT rather than with the SUT itself. Testing business logic through the presentation layer is a common example of Indirect Testing.

private final int LEGAL_CONN_MINS_SAME = 30;

public void testAnalyze_sameAirline_LessThanConnectionLimit()

throws Exception {

    // 设置

    FlightConnection illegalConn =

            createSameAirlineConn( LEGAL_CONN_MINS_SAME - 1);

    // 练习

    FlightConnectionAnalyzerImpl sut =

            new FlightConnectionAnalyzerImpl();

    String actualHtml =

            sut.getFlightConnectionAsHtmlFragment(

                           illegalConn.getInboundFlightNumber(),

                           illegalConn.getOutboundFlightNumber());

    // 验证

    StringBuffer expected = new StringBuffer();

    expected.append("<span class="boldRedText">");

    expected.append("航班之间的连接时间 ");

    expected.append(illegalConn.getInboundFlightNumber());

    expected.append(" 和航班 ");

    预期.append(illegalConn.getOutboundFlightNumber());

    预期.append(" 是 ");

    预期.append(illegalConn.getActualConnectionTime());

    预期.append(" 分钟。</span>");

    assertEquals("html", expected.toString(), actualHtml);

}

private  final  int  LEGAL_CONN_MINS_SAME  =  30;

public  void  testAnalyze_sameAirline_LessThanConnectionLimit()

throws  Exception  {

    //  setup

    FlightConnection  illegalConn  =

            createSameAirlineConn(  LEGAL_CONN_MINS_SAME  -  1);

    //  exercise

    FlightConnectionAnalyzerImpl  sut  =

            new  FlightConnectionAnalyzerImpl();

    String  actualHtml  =

            sut.getFlightConnectionAsHtmlFragment(

                           illegalConn.getInboundFlightNumber(),

                           illegalConn.getOutboundFlightNumber());

    //  verification

    StringBuffer  expected  =  new  StringBuffer();

    expected.append("<span  class="boldRedText">");

    expected.append("Connection  time  between  flight  ");

    expected.append(illegalConn.getInboundFlightNumber());

    expected.append("  and  flight  ");

    expected.append(illegalConn.getOutboundFlightNumber());

    expected.append("  is  ");

    expected.append(illegalConn.getActualConnectionTime());

    expected.append("  minutes.</span>");

    assertEquals("html",  expected.toString(),  actualHtml);

}

 
影响

通过中间对象可能无法测试 SUT 中“可能出现故障的任何东西”。事实上,这样的测试不太可能非常清晰或易于理解。它们肯定不会产生“测试即文档”

It may not be possible to test "anything that could possibly break" in the SUT via the intermediate object. Indeed, such tests are unlikely to be very clear or understandable. They certainly will not result in Tests as Documentation.

间接测试可能会导致脆弱测试,因为即使 SUT没有修改,中间对象的变化也可能需要修改测试。

Indirect Testing may result in Fragile Tests because changes in the intermediate objects may require modification of the tests even when the SUT is not modified.

根本原因

SUT 可能对用于从测试访问它的类来说是“私有的”。可能无法直接创建 SUT,因为构造函数本身是私有的。这个问题只是软件不是为可测试性而设计的迹象之一。

The SUT may be "private" to the class being used to access it from the test. It may not be possible to create the SUT directly because the constructors themselves are private. This problem is just one sign that the software is not designed for testability.

可能无法直接观察到测试结果的实际结果。在这种情况下,必须通过中间对象来验证测试的预期结果。

It may be that the actual outcome of exercising the SUT cannot be observed directly. In such a case, the expected outcome of the test must be verified through an intermediate object.

可能的解决方案

可能需要改进 SUT 的可测试性设计以消除这种异味。我们可能能够使用提取可测试组件重构(Sprout 类[WEwLC]重构的变体)将 SUT 直接暴露给测试。这种方法可能会导致不可测试的Humble 对象第 695页)和包含大部分或全部实际逻辑的易于测试的对象。

It may be necessary to improve the design-for-testability of the SUT to remove this smell. We might be able to expose the SUT directly to the test by using an Extract Testable Component refactoring (a variant of the Sprout Class [WEwLC] refactoring). This approach may result in an untestable Humble Object (page 695) and an easily tested object that contains most or all of the actual logic.

public void testAnalyze_sameAirline_EqualsConnectionLimit()

throws Exception {

    // 设置

    Mock flightMgntStub = mock(FlightManagementFacade.class);

    Flight firstFlight = createFlight();

    Flight secondFlight = createConnectingFlight(

                                    firstFlight, LEGAL_CONN_MINS_SAME);

    flightMgntStub.expects(once()).method("getFlight")

                           .with(eq(firstFlight.getFlightNumber()))

                           .will(returnValue(firstFlight));

    flightMgntStub.expects(once()).method("getFlight")

                           .with(eq(secondFlight.getFlightNumber()))

                           .will(returnValue(secondFlight));

    // 锻炼

    FlightConnAnalyzer theConnectionAnalyzer =

            new FlightConnAnalyzer();

    theConnectionAnalyzer.facade =

            (FlightManagementFacade)flightMgntStub.proxy();

    FlightConnection actualConnection =

            theConnectionAnalyzer.getConn(

                                        firstFlight.getFlightNumber(),

                                        secondFlight.getFlightNumber());

    // 验证

    assertNotNull("实际连接", actualConnection);

    assertTrue("IsLegal", actualConnection.isLegal());

}

public  void  testAnalyze_sameAirline_EqualsConnectionLimit()

throws  Exception  {

    //  setup

    Mock  flightMgntStub  =  mock(FlightManagementFacade.class);

    Flight  firstFlight  =  createFlight();

    Flight  secondFlight  =  createConnectingFlight(

                                    firstFlight,  LEGAL_CONN_MINS_SAME);

    flightMgntStub.expects(once()).method("getFlight")

                           .with(eq(firstFlight.getFlightNumber()))

                           .will(returnValue(firstFlight));

    flightMgntStub.expects(once()).method("getFlight")

                           .with(eq(secondFlight.getFlightNumber()))

                           .will(returnValue(secondFlight));

    //  exercise

    FlightConnAnalyzer  theConnectionAnalyzer  =

            new  FlightConnAnalyzer();

    theConnectionAnalyzer.facade  =

            (FlightManagementFacade)flightMgntStub.proxy();

    FlightConnection  actualConnection  =

            theConnectionAnalyzer.getConn(

                                        firstFlight.getFlightNumber(),

                                        secondFlight.getFlightNumber());

    //  verification

    assertNotNull("actual  connection",  actualConnection);

    assertTrue("IsLegal",  actualConnection.isLegal());

}

 

有时,我们可能被迫间接与 SUT 交互,因为我们无法重构代码以公开我们试图测试的逻辑。在这些情况下,我们应该将间接测试强制的复杂逻辑封装在适当命名的测试实用程序方法后面。同样,夹具设置可以隐藏在创建方法后面,结果验证可以通过验证方法隐藏(参见自定义断言)。两者都是SUT API 封装的示例(参见测试实用程序方法)。

Sometimes we may be forced to interact with the SUT indirectly because we cannot refactor the code to expose the logic we are trying to test. In these cases, we should encapsulate the complex logic forced by Indirect Testing behind suitably named Test Utility Methods. Similarly, fixture setup can be hidden behind Creation Methods and result verification can be hidden by Verification Methods (see Custom Assertion). Both are examples of SUT API Encapsulation (see Test Utility Method).

public void testAnalyze_sameAirline_LessThanConnLimit()

throws Exception {

    // 设置

    FlightConnection illegalConn =

            createSameAirlineConn( LEGAL_CONN_MINS_SAME - 1);

    FlightConnectionAnalyzerImpl sut =

            new FlightConnectionAnalyzerImpl();

    // 练习 SUT

    String actualHtml =

            sut.getFlightConnectionAsHtmlFragment(

                           illegalConn.getInboundFlightNumber(),

                           illegalConn.getOutboundFlightNumber());

    // 验证

    assertConnectionIsIllegal(illegalConn, actualHtml);

}

public  void  testAnalyze_sameAirline_LessThanConnLimit()

throws  Exception  {

    //  setup

    FlightConnection  illegalConn  =

            createSameAirlineConn(  LEGAL_CONN_MINS_SAME  -  1);

    FlightConnectionAnalyzerImpl  sut  =

            new  FlightConnectionAnalyzerImpl();

    //  exercise  SUT

    String  actualHtml  =

            sut.getFlightConnectionAsHtmlFragment(

                           illegalConn.getInboundFlightNumber(),

                           illegalConn.getOutboundFlightNumber());

    //  verification

    assertConnectionIsIllegal(illegalConn,  actualHtml);

}

 

以下自定义断言隐藏了从演示噪音中提取业务结果的丑陋之处。它是通过对测试进行简单的提取方法 [Fowler] 重构而创建的。当然,如果它在 HTML 中搜索关键字符串,而不是构建整个预期字符串并一次性进行比较,则此示例会更加健壮。其他表示层测试(请参阅第337页的层测试)可能会验证表示逻辑是否正确格式化 HTML 字符串。

The following Custom Assertion hides the ugliness of extracting the business result from the presentation noise. It was created by doing a simple Extract Method [Fowler] refactoring on the test. Of course, this example would be more robust if it searched inside the HTML for key strings rather than building up the entire expected string and comparing it all at once. Other Presentation Layer Tests (see Layer Test on page 337) might then verify that the presentation logic is formatting the HTML string properly.

private void assertConnectionIsIllegal( FlightConnection conn,

                                                          String actualHtml) {

    // 设置预期值

    StringBuffer expected = new StringBuffer();

    expected.append("<span class="boldRedText">");

    expected.append("航班之间的连接时间 ");

    expected.append(conn.getInboundFlightNumber());

    expected.append(" 和航班 ");

    expected.append(conn.getOutboundFlightNumber());

    expected.append(" 是 ");

    expected.append(conn.getActualConnectionTime());

    expected.append(" 分钟。</span>");

    // 验证

    assertEquals("html", expected.toString(), actualHtml);

}

private  void  assertConnectionIsIllegal(  FlightConnection  conn,

                                                          String  actualHtml)  {

    //  set  up  expected  value

    StringBuffer  expected  =  new  StringBuffer();

    expected.append("<span  class="boldRedText">");

    expected.append("Connection  time  between  flight  ");

    expected.append(conn.getInboundFlightNumber());

    expected.append("  and  flight  ");

    expected.append(conn.getOutboundFlightNumber());

    expected.append("  is  ");

    expected.append(conn.getActualConnectionTime());

    expected.append("  minutes.</span>");

    //  verification

    assertEquals("html",  expected.toString(),  actualHtml);

}

 

解决方案模式

Solution Patterns

良好的测试策略有助于使测试代码易于理解。然而,正如“没有作战计划能在与敌人的第一次接触中幸存下来”,没有测试基础设施可以预测所有测试的所有需求。我们应该期望测试基础设施会随着软件的成熟和我们的测试自动化技能的提高而发展。

A good test strategy helps keep the test code understandable. Nevertheless, just as "no battle plan survives the first contact with the enemy," no test infrastructure can anticipate all needs of all tests. We should expect the test infrastructure to evolve as the software matures and our test automation skills improve.

我们可以通过让多个测试调用测试实用方法或者要求通用参数化测试第 607页)传入已构建的测试装置或预期对象,将在多个场景中重用测试逻辑。

We can reuse test logic for several scenarios by having several tests call Test Utility Methods or by asking a common Parameterized Test (page 607) to pass in the already built test fixture or Expected Objects.

以“由外而内”的方式编写测试可以最大限度地减少生成可能需要重构的模糊测试的可能性。这种方法首先使用对不存在的测试实用方法的调用来概述四阶段测试。一旦我们对这些测试感到满意,我们就可以编写运行它们所需的实用方法。通过首先编写测试,我们可以更好地了解实用方法需要做什么,以使编写测试尽可能简单。当然,“测试感染者”会在编写测试实用方法之前编写测试实用测试(参见测试实用方法)。

Writing tests in an "outside-in" way can minimize the likelihood of producing an Obscure Test that might then need to be refactored. This approach starts by outlining the Four-Phase Test using calls to nonexistent Test Utility Methods. Once we are satisfied with these tests, we can write the utility methods needed to run them. By writing the tests first, we gain a better understanding of what the utility methods need to do for us to make writing the tests as simple as possible. The "test-infected" will, of course, write Test Utility Tests (see Test Utility Method) before writing the Test Utility Methods.

条件测试逻辑

Conditional Test Logic

也称为

Also known as

缩进测试代码

Indented Test Code

测试包含可能执行或可能不执行的代码。

A test contains code that may or may not be executed.

完全自动化测试(见第 26页)只是验证其他代码行为的代码。但如果此代码很复杂,我们如何验证是否正常工作?我们可以为测试编写测试 - 但这种递归何时停止?简单的答案是测试方法第 348页)必须足够简单,以至于不需要测试。

A Fully Automated Test (see page 26) is just code that verifies the behavior of other code. But if this code is complicated, how do we verify that it works properly? We could write tests for our tests—but when would this recursion stop? The simple answer is that Test Methods (page 348) must be simple enough to not need tests.

条件测试逻辑是使测试比实际更复杂的一个因素。

Conditional Test Logic is one factor that makes tests more complicated than they really should be.

症状

Symptoms

作为一种代码异味,条件测试逻辑可能不会产生任何行为症状,但测试读者应该相当明显地注意到它的存在。以极度怀疑的态度查看测试方法中的任何控制结构!测试读者可能还想知道正在执行的代码路径是哪条。以下是涉及循环和语句的条件测试逻辑if的示例:

As a code smell, Conditional Test Logic may not produce any behavioral symptoms but its presence should be reasonably obvious to the test reader. View any control structures within a Test Method with extreme suspicion! The test reader may also wonder which code path is the one that is being executed. The following is an example of Conditional Test Logic that involves both looping and if statements:

      // 验证温哥华是否在列表中

      actual = null;

      i = flightsFromCalgary.iterator();

      while (i.hasNext()) {

          FlightDto flightDto = (FlightDto) i.next();

          if (flightDto.getFlightNumber().equals(

                  expectedCalgaryToVan.getFlightNumber()))

          {

              actual = flightDto;

              assertEquals("从卡尔加里飞往温哥华的航班",

                                expectedCalgaryToVan,

                                flightDto);

              break;

          }

      }

}

      //     verify  Vancouver  is  in  the  list

      actual  =  null;

      i  =  flightsFromCalgary.iterator();

      while  (i.hasNext())  {

          FlightDto  flightDto  =  (FlightDto)  i.next();

          if  (flightDto.getFlightNumber().equals(

                  expectedCalgaryToVan.getFlightNumber()))

          {

              actual  =  flightDto;

              assertEquals("Flight  from  Calgary  to  Vancouver",

                                expectedCalgaryToVan,

                                flightDto);

              break;

          }

      }

}

 

这段代码引出了一个问题:“这段测试代码在做什么?我们如何知道它做得正确?” 一个行为症状可能是存在相关的项目级异味高测试维护成本(第265页),这可能是由条件测试逻辑引入的复杂性引起的。

This code begs the question, "What is this test code doing and how do we know that it is doing it correctly?" One behavioral symptom may be the presence of the related project-level smell High Test Maintenance Cost (page 265), which may be caused by the complexity introduced by the Conditional Test Logic.

影响

Impact

条件测试逻辑使得在真正重要的时候很难确切测试将要做什么。只有一条执行路径的代码总是以完全相同的方式执行。具有多条执行路径的代码带来了更大的挑战,并且对其结果的信心也较低。

Conditional Test Logic makes it difficult to know exactly what a test is going to do when it really matters. Code that has only a single execution path always executes in exactly the same way. Code that has multiple execution paths presents much greater challenges and does not inspire as much confidence about its outcome.

为了增强对生产代码的信心,我们可以编写自检测试(见第26页)来测试代码。如果每次运行测试代码时执行的结果都不一样,我们如何才能增强对测试代码的信心呢?很难知道(或证明)测试是否在验证我们希望它验证的行为。具有分支或循环的测试,或者每次运行时使用不同值的测试,可能很难调试,因为它不是完全确定的。

To increase our confidence in production code, we can write Self-Checking Tests (see page 26) that exercise the code. How can we increase our confidence in the test code if it executes differently each time we run it? It is hard to know (or prove) that the test is verifying the behavior we want it to verify. A test that has branches or loops, or that uses different values each time it is run, can be very difficult to debug simply because it isn't completely deterministic.

一个相关的问题是,条件测试逻辑使得正确编写测试变得更加困难。由于测试本身无法轻松测试,我们如何知道它实际上会检测到它想要捕获的错误?[这是模糊测试(第 186页) 的常见问题;与简单代码相比,它们更有可能导致错误测试(第 260页)。]

A related issue is that Conditional Test Logic makes writing the test correctly a more difficult task. Because the test itself cannot be tested easily, how do we know that it will actually detect the bugs it is intended to catch? [This is a general problem with Obscure Tests (page 186); they are more likely to result in Buggy Tests (page 260) than simple code.]

原因

Causes

测试自动化人员可能出于以下几个原因引入条件测试逻辑

Test automaters may introduce Conditional Test Logic for several reasons:

  • 当 SUT 无法返回有效数据时,他们可能会使用if语句来引导执行fail或避免执行某些测试代码。
  • They may use if statements to steer execution to a fail statement or to avoid executing certain pieces of test code when the SUT fails to return valid data.
  • 他们可能会使用循环来验证对象集合的内容(条件验证逻辑)。这也可能导致模糊测试
  • They may use loops to verify the contents of collections of objects (Conditional Verification Logic). This may also result in an Obscure Test.
  • 他们可能会使用条件测试逻辑来验证复杂对象或多态数据结构(条件验证逻辑的另一种形式)。这只是该equals方法的外部方法 [Fowler] 实现。
  • They may use Conditional Test Logic to verify complex objects or polymorphic data structures (another form of Conditional Verification Logic). This is just a Foreign Method [Fowler] implementation of the equals method.
  • 他们可以使用条件测试逻辑来初始化测试装置或预期对象(请参阅第462页的状态验证),以便他们可以重用单个测试来验证几种不同的情况(灵活测试)。
  • They may use Conditional Test Logic to initialize the test fixture or Expected Object (see State Verification on page 462) so they can reuse a single test to verify several different cases (Flexible Test).
  • 他们可以使用if语句来避免拆除不存在的固定装置对象(复杂拆除)。
  • They may use if statements to avoid tearing down nonexistent fixture objects (Complex Teardown).

其中一些原因值得更详细地研究。

Some of these causes are worth examining in more detail.

原因:灵活测试

测试代码根据运行的时间和地点验证不同的功能。

The test code verifies different functionality depending on when or where it is run.

症状

测试包含条件逻辑,根据当前环境执行不同的操作。最常见的是,此功能采用条件测试逻辑的形式,根据测试外部的一些因素构建预期结果的不同版本。

The test contains conditional logic that does different things depending on the current environment. Most commonly this functionality takes the form of Conditional Test Logic to build different versions of the expected results based on some factor external to the test.

考虑以下测试,它获取当前时间,以便确定 SUT 的输出应该是什么:

Consider the following test, which gets the current time so that it can determine what the output of the SUT should be:

public void testDisplayCurrentTime_whenever() {

     // 固定设置

     TimeDisplay sut = new TimeDisplay();

     // 练习 SUT

     String result = sut.getCurrentTimeAsHtmlFragment();

     // 验证结果

     Calendar time = new DefaultTimeProvider().getTime();

     StringBuffer expectedTime = new StringBuffer();

     expectedTime.append("<span class=\"tinyBoldText\">");

     if ((time.get(Calendar.HOUR_OF_DAY) == 0)

         && (time.get(Calendar.MINUTE) <= 1)) {

         expectedTime.append( "午夜");

     } else if ((time.get(Calendar.HOUR_OF_DAY) == 12)

                     && (time.get(Calendar.MINUTE) == 0)) { // 中午

         expectedTime.append("中午");

     } else {

         SimpleDateFormat fr = new SimpleDateFormat("h:mm a");

         expectedTime.append(fr.format(time.getTime()));

     }

     expectedTime.append("</span>");

     assertEquals( expectedTime, result);

}

public  void  testDisplayCurrentTime_whenever()  {

     //  fixture  setup

     TimeDisplay  sut  =  new  TimeDisplay();

     //  exercise  SUT

     String  result  =  sut.getCurrentTimeAsHtmlFragment();

     //  verify  outcome

     Calendar  time  =  new  DefaultTimeProvider().getTime();

     StringBuffer  expectedTime  =  new  StringBuffer();

     expectedTime.append("<span  class=\"tinyBoldText\">");

     if  ((time.get(Calendar.HOUR_OF_DAY)  ==  0)

         &&  (time.get(Calendar.MINUTE)  <=  1))  {

         expectedTime.append(  "Midnight");

     }  else  if  ((time.get(Calendar.HOUR_OF_DAY)  ==  12)

                     &&  (time.get(Calendar.MINUTE)  ==  0))  {  //  noon

         expectedTime.append("Noon");

     }  else    {

         SimpleDateFormat  fr  =  new  SimpleDateFormat("h:mm  a");

         expectedTime.append(fr.format(time.getTime()));

     }

     expectedTime.append("</span>");

     assertEquals(  expectedTime,  result);

}

 
根本原因

灵活性测试是由于缺乏对环境的控制而导致的。测试自动化程序可能无法将 SUT 与其依赖项分离,并决定根据环境状态调整测试逻辑。

A Flexible Test is caused by a lack of control of the environment. The test automater probably wasn't able to decouple the SUT from its dependencies and decided to adapt the test logic based on the state of the environment.

影响

第一个问题是使用灵活测试会使测试更难理解,因此也更难维护。第二个问题是我们不知道哪些测试场景实际上正在执行,以及是否所有场景实际上都在定期执行。例如,在我们的示例测试中,午夜场景是否执行过?多久执行一次?可能很少,因为测试必须在午夜准时运行 — 即使我们将夜间构建的时间安排在午夜,这也是不太可能发生的事情。

The first issue is that using a Flexible Test makes the test harder to understand and therefore to maintain. The second issue is that we don't know which test scenarios are actually being exercised and whether all scenarios are, in fact, exercised regularly. For example, in our sample test, is the midnight scenario ever exercised? How often? Probably rarely, if ever, because the test would have to be run at exactly midnight—an unlikely event, even if we timed the nightly build such that it ran over midnight.

可能的解决方案

实现灵活测试的最佳方法是将 SUT 与促使测试自动化程序实现测试灵活性的任何依赖项分离。这涉及重构 SUT 以支持可替代依赖项。然后,我们可以用测试替身第 522页)替换依赖项,例如测试桩第 529页)或模拟对象第 544页),并为灵活测试之前涵盖的每种情况编写单独的测试。

A Flexible Test is best addressed by decoupling the SUT from whatever dependencies prompted the test automater to make the test flexible. This involves refactoring the SUT to support substitutable dependency. We can then replace the dependency with a Test Double (page 522), such as a Test Stub (page 529) or Mock Object (page 544), and write separate tests for each circumstance previously covered by the Flexible Test.

原因:条件验证逻辑

条件测试逻辑第 200)在用于验证预期结果时也可能产生问题。当测试人员试图在 SUT 未能返回正确对象时阻止执行断言或使用循环来验证 SUT 返回的集合内容时,通常会出现此问题。

Conditional Test Logic (page 200) may also create problems when it is used to verify the expected outcome. This issue usually arises when the tester tries to prevent the execution of assertions if the SUT fails to return the right objects or uses loops to verify the contents of collections returned by the SUT.

      // 验证温哥华是否在列表中

      actual = null;

      i = flightsFromCalgary.iterator();

      while (i.hasNext()) {

          FlightDto flightDto = (FlightDto) i.next();

          if (flightDto.getFlightNumber().equals(

                  expectedCalgaryToVan.getFlightNumber()))

          {

              actual = flightDto;

              assertEquals("从卡尔加里飞往温哥华的航班",

                                expectedCalgaryToVan,

                                flightDto);

              break;

          }

      }

}

      //     verify  Vancouver  is  in  the  list

      actual  =  null;

      i  =  flightsFromCalgary.iterator();

      while  (i.hasNext())  {

          FlightDto  flightDto  =  (FlightDto)  i.next();

          if  (flightDto.getFlightNumber().equals(

                  expectedCalgaryToVan.getFlightNumber()))

          {

              actual  =  flightDto;

              assertEquals("Flight  from  Calgary  to  Vancouver",

                                expectedCalgaryToVan,

                                flightDto);

              break;

          }

      }

}

 
可能的解决方案

我们可以用Guard Assertion第 490if页)替换引导执行到调用的语句,这样在到达我们不想执行的代码之前,测试就会失败。除非测试是预期异常测试(请参阅测试方法),否则这种方法很有效。在后一种情况下,我们应该使用 xUnit 家族成员和语言的标准预期异常测试编码习惯用法。fail

We can replace the if statements that steer execution to a call to fail with a Guard Assertion (page 490) that causes the test to fail before we reach the code we don't want to execute. This works well unless the test is an Expected Exception Test (see Test Method.) In the latter case, we should use the standard Expected Exception Test coding idiom for the xUnit family member and language.

我们可以用预期对象的相等性断言(参见第 362页的断言方法)来代替用于验证复杂对象的条件测试逻辑。如果生产代码的方法过于严格,我们可以使用自定义断言第 474页)来定义特定于测试的相等性equals

We can replace Conditional Test Logic for verification of complex objects with an Equality Assertion (see Assertion Method on page 362) on an Expected Object. If the production code's equals method is too strict, we can use a Custom Assertion (page 474) to define test-specific equality.

我们应该将验证逻辑中的任何循环移至自定义断言。然后,我们可以使用自定义断言测试来验证此断言的行为(请参阅自定义断言)。

We should move any loops in the verification logic to a Custom Assertion. We can then verify this assertion's behavior by using Custom Assertion Tests (see Custom Assertion).

我们可以通过调用测试实用程序方法第 599页)或传入已构建的测试装置和预期对象第 607页)的通用参数化测试,在多个测试中重用测试逻辑。

We can reuse test logic in several tests by calling a Test Utility Method (page 599) or a common Parameterized Test (page 607) that passes in the already built test fixture and Expected Objects.

原因:测试中的生产逻辑
症状

在我们的测试结果验证部分中可以发现一些条件测试逻辑的形式。让我们更仔细地看看这个测试的循环内部:

Some forms of Conditional Test Logic are found in the result verification section of our tests. Let us look more closely inside the loops of this test:

public void testCombinationsOfInputValues() {

    // 设置夹具

    计算器 sut = new Calculator();

    int expected; // 循环内有待解决



    for (int i = 0; i < 10; i++) {

        for (int j = 0; j < 10; j++) {

            // 练习 SUT

            int actual = sut.calculate( i, j );



            // 验证结果

            if (i==3 & j==4) // 特殊情况

                expected = 8;

            else

                expected = i+j;



            assertEquals(message(i,j), expected, actual);

        }

    }

}



private String message(int i, int j) {

    return "Cell( " + String.valueOf(i)+ ","

                            + String.valueOf(j) + ")";

}

public  void  testCombinationsOfInputValues()  {

    //  Set  up  fixture

    Calculator  sut  =  new  Calculator();

    int  expected;    //  TBD  inside  loops



    for  (int  i  =  0;  i  <  10;  i++)  {

        for  (int  j  =  0;  j  <  10;  j++)  {

            //  Exercise  SUT

            int  actual  =  sut.calculate(  i,  j  );



            //  Verify  result

            if  (i==3  &  j==4)    //  special  case

                expected  =  8;

            else

                expected  =  i+j;



            assertEquals(message(i,j),  expected,  actual);

        }

    }

}



private  String  message(int  i,  int  j)  {

    return  "Cell(  "  +  String.valueOf(i)+  ","

                            +  String.valueOf(j)  +  ")";

}

 

此循环驱动测试(参见参数化测试)中的嵌套循环使用和值的各种组合i作为输入来测试 SUT j。这里我们将重点介绍循环内的条件测试逻辑。

The nested loops in this Loop-Driven Test (see Parameterized Test) exercise the SUT with various combinations of values of i and j as inputs. Here we will focus on the Conditional Test Logic inside the loop.

根本原因

测试中的这种生产逻辑是想要在单个测试方法中验证多个测试条件的直接结果。鉴于有多个输入值传递给 SUT,我们也应该有多个预期结果。如果我们在嵌套循环中将多个输入参数的多种组合传递给 SUT,则很难枚举每组输入的预期结果。此问题的一个常见解决方案是根据输入使用计算值(请参阅第718页的派生值)。潜在的缺点(如我们在此处看到的)是,我们发现自己在测试中复制预期的 SUT 逻辑来计算断言的预期值。

This Production Logic in Test is a direct result of wanting to verify multiple test conditions in a single Test Method. Given that multiple input values are passed to the SUT, we should also have multiple expected results. It is hard to enumerate the expected result for each set of inputs if we pass in many combinations of several input arguments to the SUT in nested loops. A common solution to this problem is to use a Calculated Value (see Derived Value on page 718) based on the inputs. The potential downfall (as we see here) is that we find ourselves replicating the expected SUT logic inside our test to calculate the expected values for assertions.

可能的解决方案

如果可能的话,最好枚举用于测试 SUT 的预计算值集。以下示例使用一组(较小的)枚举值测试相同的逻辑:

If at all possible, it is better to enumerate the sets of precalculated values with which to test the SUT. The following example tests the same logic using a (smaller) set of enumerated values:

public void testMultipleValueSets() {

    // 设置夹具

    计算器 sut = new Calculator();

    TestValues[] testValues = {

                          new TestValues(1,2,3),

                          new TestValues(2,3,5),

                          new TestValues(3,4,8), // 特殊情况!

                          new TestValues(4,5,9)

                                              };



    for (int i = 0; i < testValues.length; i++) {

        TestValues values = testValues[i];

        // 练习 SUT

        int actual = sut.calculate( values.a, values.b);

        // 验证结果

        assertEquals(message(i), values.expectedSum, actual);

    }

}



private String message(int i) {

    return "Row "+ String.valueOf(i);

}

public  void  testMultipleValueSets()  {

    //  Set  Up  Fixture

    Calculator  sut  =  new  Calculator();

    TestValues[]  testValues  =  {

                          new  TestValues(1,2,3),

                          new  TestValues(2,3,5),

                          new  TestValues(3,4,8),  //  special  case!

                          new  TestValues(4,5,9)

                                              };



    for  (int  i  =  0;  i  <  testValues.length;  i++)  {

        TestValues  values  =  testValues[i];

        //  Exercise  SUT

        int  actual  =  sut.calculate(  values.a,  values.b);

        //  Verify  Result

        assertEquals(message(i),  values.expectedSum,  actual);

    }

}



private  String  message(int  i)  {

    return  "Row  "+  String.valueOf(i);

}

 
原因:拆卸复杂
症状

如果复杂的装置拆卸代码不能正确地进行自我清理,则更有可能破坏测试环境。很难验证拆卸代码是否正确编写,并且此类代码很容易导致“数据泄露”,这可能会导致此测试或其他测试无缘无故地失败。请考虑以下示例:

Complex fixture teardown code is more likely to leave the test environment corrupted if it does not clean up after itself correctly. It is hard to verify that teardown code has been written correctly, and such code can easily result in "data leaks" that may later cause this or other tests to fail for no apparent reason. Consider this example:

public void testGetFlightsByOrigin_NoInboundFlight_SMRTD()

              throws Exception {

      // 设置 Fixture

      BigDecimal outboundAirport = createTestAirport("1OF");

      BigDecimal inboundAirport = null;

      FlightDto expFlightDto = null;

      try {

          inboundAirport = createTestAirport("1IF");

          expFlightDto =

                  createTestFlight(outboundAirport, inboundAirport);

            // 练习系统

            列表 flightsAtDestination1 =

                    Facade.getFlightsByOriginAirport(inboundAirport);

            // 验证结果

            assertEquals(0,flightsAtDestination1.size());

      } finally {

            try {

                Facade.removeFlight(expFlightDto.getFlightNumber());

            } finally {

                  try {

                      Facade.removeAirport(inboundAirport);

                  } finally {

                        Facade.removeAirport(outboundAirport);

                  }

            }

      }

}

public  void  testGetFlightsByOrigin_NoInboundFlight_SMRTD()

              throws  Exception  {

      //  Set  Up  Fixture

      BigDecimal  outboundAirport  =  createTestAirport("1OF");

      BigDecimal  inboundAirport  =  null;

      FlightDto  expFlightDto  =  null;

      try  {

          inboundAirport  =  createTestAirport("1IF");

          expFlightDto  =

                  createTestFlight(outboundAirport,  inboundAirport);

            //  Exercise  System

            List  flightsAtDestination1  =

                    facade.getFlightsByOriginAirport(inboundAirport);

            //  Verify  Outcome

            assertEquals(0,flightsAtDestination1.size());

      }  finally  {

            try  {

                facade.removeFlight(expFlightDto.getFlightNumber());

            }  finally  {

                  try  {

                      facade.removeAirport(inboundAirport);

                  }  finally    {

                        facade.removeAirport(outboundAirport);

                  }

            }

      }

}

 
根本原因

通常,只有当我们使用的持久性资源超出了垃圾回收系统的范围时,才需要进行拆卸。当在同一测试方法中使用许多此类资源时,就会发生复杂拆卸

Teardown is typically required only when we use persistent resources that are beyond the reach of our garbage collection system. Complex Teardown occurs when many such resources are used in the same Test Method.

可能的解决方案

为了避免复杂的拆卸逻辑,我们应该使用隐式拆卸第 516页),这将使代码既可重用又可测试,或者使用自动拆卸第 503页),这可以通过自动单元测试进行验证。我们还可以使用Fresh Fixture第 311页)策略来消除拆卸任何 Fixture 对象的需要,并使用某种测试替身来避免在测试中使用任何持久对象。

To avoid complex teardown logic, we should use Implicit Teardown (page 516), which will make the code both reusable and testable, or Automated Teardown (page 503), which can be verified with automated unit tests. We can also eliminate the need to tear down any fixture objects by using a Fresh Fixture (page 311) strategy and by avoiding the use of any persistent objects in our tests by using some sort of Test Double.

原因:多重测试条件
症状

测试会尝试将相同的测试逻辑应用于多组输入值,每组输入值都有各自的对应预期结果。在以下示例中,测试会遍历一组测试值并将测试逻辑应用于每组输入值:

A test tries to apply the same test logic to many sets of input values, each with its own corresponding expected result. In the following example, the test iterates over a collection of test values and applies the test logic to each set:

public void testMultipleValueSets() {

     // 设置夹具

     计算器 sut = new Calculator();

     TestValues[] testValues = {

                            new TestValues(1,2,3),

                            new TestValues(2,3,5),

                            new TestValues(3,4,8), // 特殊情况!

                            new TestValues(4,5,9)

                                              };



    for (int i = 0; i < testValues.length; i++) {

        TestValues values = testValues[i];

        // 练习 SUT

        int actual = sut.calculate( values.a, values.b);

        // 验证结果

        assertEquals(message(i), values.expectedSum, actual);

    }

}



private String message(int i) {

    return "Row "+ String.valueOf(i);

}

public  void  testMultipleValueSets()  {

     //  Set  Up  Fixture

     Calculator  sut  =  new  Calculator();

     TestValues[]  testValues  =  {

                            new  TestValues(1,2,3),

                            new  TestValues(2,3,5),

                            new  TestValues(3,4,8),  //  special  case!

                            new  TestValues(4,5,9)

                                              };



    for   (int  i  =  0;  i  <  testValues.length;  i++)  {

        TestValues  values  =  testValues[i];

        //  Exercise  SUT

        int  actual  =  sut.calculate(  values.a,  values.b);

        //  Verify  Outcome

        assertEquals(message(i),  values.expectedSum,  actual);

    }

}



private  String  message(int  i)  {

    return  "Row  "+  String.valueOf(i);

}

 
根本原因

测试自动化程序试图在单个测试方法中使用相同的测试逻辑来测试许多测试条件。在前面的例子中,它是相当简单的条件测试逻辑if。如果代码包含多个嵌套循环,甚至可能包含计算预期值不同情况的语句,情况可能会更糟。

The test automater is trying to test many test conditions using the same test logic in a single Test Method. In the preceding example, it is fairly simple Conditional Test Logic. Matters could be a lot worse if the code contained multiple nested loops and maybe even if statements to calculate different cases of the expected values.

可能的解决方案

在所有条件测试逻辑来源中,多重测试条件可能是最无害的。除了吓到测试读者之外,这种测试的主要影响是它在第一次失败时停止执行,并且当代码中出现错误时不提供缺陷定位(参见第22页)。通过使用提取方法 [Fowler] 重构在循环内创建参数化测试调用,可以轻松解决可读性问题。可以通过为每个测试条件从单独的测试方法调用参数化测试来解决缺陷定位不足的问题。对于大量值,数据驱动测试第 288页)可能是更好的解决方案。

Of all sources of Conditional Test Logic, Multiple Test Conditions is probably the most innocuous. Other than scaring the test reader, the main impact of such a test is that it stops executing at the first failure and doesn't provide Defect Localization (see page 22) when a bug is introduced into the code. The readability issue can easily be addressed by using an Extract Method [Fowler] refactoring to create a Parameterized Test call from within the loop. The lack of Defect Localization can be addressed by calling the Parameterized Test from a separate Test Method for each test condition. For large sets of values, a Data-Driven Test (page 288) might be a better solution.

难以测试的代码

Hard-to-Test Code

代码难以测试。

Code is difficult to test.

自动化测试是一种强大的工具,即使我们需要维护大量代码库,它也能帮助我们快速开发软件。当然,只有当我们的大部分代码都受到全自动测试的保护时,它才能提供这些好处(见第26页)。编写这些测试的工作量必须加到编写它们验证的产品代码的工作量中。毫不奇怪,我们希望让编写自动化测试变得容易。3

Automated testing is a powerful tool that helps us develop software quickly even when we have a large code base to maintain. Of course, it provides these benefits only if most of our code is protected by Fully Automated Tests (see page 26). The effort of writing these tests must be added to the effort of writing the product code they verify. Not surprisingly, we would prefer to make it easy to write the automated tests.3

难以测试的代码是导致难以以经济高效的方式编写完整、正确的自动化测试的一个因素。

Hard-to-Test Code is one factor that makes it difficult to write complete, correct automated tests in a cost-efficient manner.

症状

Symptoms

有些类型的代码天生就难以测试,例如 GUI 组件、多线程代码和测试代码。可能很难获取要测试的代码,因为它对测试不可见。编译测试可能会有问题,因为代码与其他类的耦合度太高。创建对象的实例可能很困难,因为构造函数不存在、是私有的,或者需要太多其他对象作为参数。

Some kinds of code are inherently difficult to test—GUI components, multithreaded code, and test code, for example. It may be difficult to get at the code to be tested because it is not visible to a test. It may be problematic to compile a test because the code is too highly coupled to other classes. It may be hard to create an instance of the object because the constructors don't exist, are private, or take too many other objects as parameters.

影响

Impact

每当我们遇到难以测试的代码时,我们就无法轻松地以自动化方式验证该代码的质量。虽然手动质量评估通常是可行的,但它的可扩展性并不好,因为每次代码更改后执行此评估的工作通常意味着它无法完成。而且如果不花费大量的测试文档成本,这种策略也不容易重复。

Whenever we have Hard-to-Test Code, we cannot easily verify the quality of that code in an automated way. While manual quality assessment is often possible, it doesn't scale very well because the effort to perform this assessment after each code change usually means it doesn't get done. Nor is this strategy readily repeated without a large test documentation cost.

解决方案模式

Solution Patterns

更好的解决方案是让代码更易于测试。这个主题足够大,值得用一整章来讨论,但本节只介绍其中的一些重点。

A better solution is to make the code more amenable to testing. This topic is big enough that it warrants a whole chapter of its own, but this section covers a few of the highlights.

原因

Causes

也称为

Also known as

硬编码依赖关系

Hard-Coded Dependency

代码难以测试的原因有很多;这里讨论了最常见的原因。

There are a number of reasons for Hard-to-Test Code; the most common causes are discussed here.

原因:高度耦合的代码
症状

如果不测试其他几个类,就无法测试一个类。

A class cannot be tested without also testing several other classes.

影响

与其他代码高度耦合的代码很难进行单元测试,因为它无法单独执行。

Code that is highly coupled to other code is very difficult to unit test because it won't execute in isolation.

根本原因

高度耦合的代码可能由多种因素造成,包括设计不佳、缺乏面向对象的设计经验以及缺乏鼓励解耦的奖励结构。

Highly Coupled Code can be caused by many factors, including poor design, lack of object-oriented design experience, and lack of a reward structure that encourages decoupling.

可能的解决方案

测试过度耦合代码的关键是打破耦合。当我们进行测试驱动开发时,这自然会发生。

The key to testing overly coupled code is to break the coupling. This happens naturally when we are doing test-driven development.

为了测试目的,我们经常使用一种技术来解耦代码,即测试替身第 522页),更具体地说是测试桩第 529页)或模拟对象(第544页)。第 11 章使用测试替身”将更详细地介绍此主题。

A technique that we often use to decouple code for the purpose of testing is a Test Double (page 522) or, more specifically, a Test Stub (page 529) or Mock Object (page 544). This topic is covered in much more detail in Chapter 11, Using Test Doubles.

将测试改造到现有代码上是一项更具挑战性的任务,尤其是在处理遗留代码库时。这是一个很大的话题,Michael Feathers 写了一本关于如何做到这一点的书,名为《有效使用遗留代码》 [WEwLC]

Retrofitting tests onto existing code is a more challenging task, especially when we are dealing with a legacy code base. This is a big enough topic that Michael Feathers wrote a whole book on techniques for doing this, titled Working Effectively with Legacy Code [WEwLC].

原因:异步代码
症状

无法通过直接方法调用来测试类。测试必须启动可执行文件(例如线程、进程或应用程序)并等到其启动完成后才能与可执行文件交互。

A class cannot be tested via direct method calls. The test must start an executable (such as a thread, process, or application) and wait until its start-up has finished before interacting with the executable.

影响

具有异步接口的代码很难测试,因为这些元素的测试必须与 SUT 的执行协调一致。这一要求可能会给测试增加很多复杂性,并导致测试需要更长的时间才能运行。后一个问题是单元测试的主要问题,单元测试必须运行得非常快,以确保开发人员经常运行它们。

Code that has an asynchronous interface is hard to test because the tests of these elements must coordinate their execution with that of the SUT. This requirement can add a lot of complexity to the tests and causes them to take much, much longer to run. The latter issue is a major concern with unit tests, which must run very quickly to ensure that developers will run them frequently.

根本原因

实现我们想要测试的算法的代码与它通常执行的活动对象高度耦合。

The code that implements the algorithm we wish to test is highly coupled to the active object in which it normally executes.

可能的解决方案

测试异步代码的关键是将逻辑与异步访问机制分开。可测试性设计模式Humble Object (第 695页;包括Humble DialogHumble Executable)是一个很好的例子,它说明了如何重构异步代码,以便以同步方式进行测试。

The key to testing asynchronous code is to separate the logic from the asynchronous access mechanism. The design-for-testability pattern Humble Object (page 695; including Humble Dialog and Humble Executable) is a good example of a way to restructure otherwise asynchronous code so it can be tested in a synchronous manner.

原因:不可测试的测试代码
症状

测试方法第 348页)的主体足够模糊(模糊测试;参见第 186页)或包含足够的条件测试逻辑(第200页),以至于我们怀疑测试是否正确。

The body of a Test Method (page 348) is obscure enough (Obscure Test; see page 186) or contains enough Conditional Test Logic (page 200) that we wonder whether the test is correct.

影响

测试方法中的任何条件测试逻辑都有可能产生错误测试第 260页),并可能导致高测试维护成本(第265页)。测试方法主体中的代码过多会使测试难以理解且难以正确构建。

Any Conditional Test Logic within a Test Method has a higher probability of producing Buggy Tests (page 260) and will likely result in High Test Maintenance Cost (page 265). Too much code in the test method body can make the test hard to understand and hard to construct correctly.

根本原因

测试方法主体内的代码本质上很难使用自检测试进行测试(参见第 26页)。为此,我们必须用注入目标错误的测试替身替换 SUT,然后在另一个预期异常测试(参见测试方法)方法中运行测试方法— 除了最不寻常的情况外,在其他情况下,这样做太麻烦了。

The code within the body of the Test Method is inherently hard to test using a Self-Checking Test (see page 26). To do so, we would have to replace the SUT with a Test Double that injects the target error and then run the test method inside another Expected Exception Test (see Test Method) method—much too much trouble to bother with in all but the most unusual circumstances.

可能的解决方案

我们可以使测试方法变得极其简单,并将其中的条件测试逻辑重新定位到测试实用程序方法第 599页)中,从而消除了测试方法主体的测试需要,这样我们可以轻松地编写自检测试

We can remove the need to test the body of a Test Method by making it extremely simple and relocating any Conditional Test Logic from it into Test Utility Methods (page 599), for which we can easily write Self-Checking Tests.

测试代码重复

Test Code Duplication

相同的测试代码重复多次。

The same test code is repeated many times.

套件中的许多测试需要做类似的事情。例如,测试经常演练同一主题的变体场景。测试可能需要类似的装置设置或结果验证逻辑。在某些情况下,甚至许多测试的演练 SUT 阶段也涉及重复相同的非平凡逻辑。

Many of the tests in a suite need to do similar things. For example, tests often exercise scenarios that are variations on the same theme. Tests may require similar fixture setup or result verification logic. In some cases, even the exercise SUT phase of many tests involves repeating the same nontrivial logic.

需要测试做类似的事情常常会导致测试代码重复。

The need for tests to do similar things often results in Test Code Duplication.

症状

Symptoms

多个测试可能包含本质上相同的语句的公共子集,如下例所示:

Several tests may contain a common subset of essentially the same statements, as in the following example:

public void testInvoice_addOneLineItem_quantity1_b() {

        // 练习

        inv.addItemQuantity(product, QUANTITY);

        // 验证

        列表 lineItems = inv.getLineItems();

        assertEquals("number of items", lineItems.size(), 1);

        // 仅验证项目

        LineItem expItem = new LineItem(inv, product, QUANTITY);

        LineItem actual = (LineItem)lineItems.get(0);

        assertEquals(expItem.getInv(), actual.getInv());

        assertEquals(expItem.getProd(), actual.getProd());

        assertEquals(expItem.getQuantity(), actual.getQuantity());

}



public void testRemoveLineItemsForProduct_oneOfTwo() {

        // 设置

        发票 inv = createAnonInvoice();

        inv.addItemQuantity(product, QUANTITY);

        inv.addItemQuantity(anotherProduct, QUANTITY);

        LineItem expItem = new LineItem(inv, product, QUANTITY);

        // 练习

        inv.removeLineItemForProduct(anotherProduct);

        // 验证

        列表 lineItems = inv.getLineItems();

        assertEquals("项目数量", lineItems.size(), 1);

        LineItem actual = (LineItem)lineItems.get(0);

        assertEquals(expItem.getInv(), actual.getInv());

        assertEquals(expItem.getProd(), actual.getProd());

        assertEquals(expItem.getQuantity(), actual.getQuantity());

}

public  void  testInvoice_addOneLineItem_quantity1_b()  {

        //  Exercise

        inv.addItemQuantity(product,  QUANTITY);

        //  Verify

        List  lineItems  =  inv.getLineItems();

        assertEquals("number  of  items",  lineItems.size(),  1);

        //  Verify  only  item

        LineItem  expItem  =  new  LineItem(inv,  product,  QUANTITY);

        LineItem  actual  =  (LineItem)lineItems.get(0);

        assertEquals(expItem.getInv(),  actual.getInv());

        assertEquals(expItem.getProd(),  actual.getProd());

        assertEquals(expItem.getQuantity(),  actual.getQuantity());

}



public  void  testRemoveLineItemsForProduct_oneOfTwo()  {

        //  Set  up

        Invoice  inv  =  createAnonInvoice();

        inv.addItemQuantity(product,  QUANTITY);

        inv.addItemQuantity(anotherProduct,  QUANTITY);

        LineItem  expItem  =  new  LineItem(inv,  product,  QUANTITY);

        //  Exercise

        inv.removeLineItemForProduct(anotherProduct);

        //  Verify

        List  lineItems  =  inv.getLineItems();

        assertEquals("number  of  items",  lineItems.size(),  1);

        LineItem  actual  =  (LineItem)lineItems.get(0);

        assertEquals(expItem.getInv(),  actual.getInv());

        assertEquals(expItem.getProd(),  actual.getProd());

        assertEquals(expItem.getQuantity(),  actual.getQuantity());

}

 

单个测试也可能包含重复的类似语句组:

A single test may also contain repeated groups of similar statements:

public void testInvoice_addTwoLineItems_sameProduct() {

      Invoice inv = createAnonInvoice();

      LineItem expItem1 = new LineItem(inv, product, QUANTITY1);

      LineItem expItem2 = new LineItem(inv, product, QUANTITY2);

      // 练习

      inv.addItemQuantity(product, QUANTITY1);

      inv.addItemQuantity(product, QUANTITY2);

      // 验证

      列表 lineItems = inv.getLineItems();

      assertEquals("项目数量", lineItems.size(), 2);

      // 验证第一项

      LineItem actual = (LineItem)lineItems.get(0);

      assertEquals(expItem1.getInv(), actual.getInv());

      assertEquals(expItem1.getProd(), actual.getProd());

      assertEquals(expItem1.getQuantity(), actual.getQuantity());

      // 验证第二项

      actual = (LineItem)lineItems.get(1);

      assertEquals(expItem2.getInv(), actual.getInv());

      assertEquals(expItem2.getProd(), actual.getProd());

      assertEquals(expItem2.getQuantity(), actual.getQuantity());

}

public  void  testInvoice_addTwoLineItems_sameProduct()  {

      Invoice  inv  =  createAnonInvoice();

      LineItem  expItem1  =  new  LineItem(inv,  product,  QUANTITY1);

      LineItem  expItem2  =  new  LineItem(inv,  product,  QUANTITY2);

      //  Exercise

      inv.addItemQuantity(product,  QUANTITY1);

      inv.addItemQuantity(product,  QUANTITY2);

      //  Verify

      List  lineItems  =  inv.getLineItems();

      assertEquals("number  of  items",  lineItems.size(),  2);

      //      Verify  first  item

      LineItem  actual  =  (LineItem)lineItems.get(0);

      assertEquals(expItem1.getInv(),  actual.getInv());

      assertEquals(expItem1.getProd(),  actual.getProd());

      assertEquals(expItem1.getQuantity(),  actual.getQuantity());

      //      Verify  second  item

      actual  =  (LineItem)lineItems.get(1);

      assertEquals(expItem2.getInv(),  actual.getInv());

      assertEquals(expItem2.getProd(),  actual.getProd());

      assertEquals(expItem2.getQuantity(),  actual.getQuantity());

}

 

上述两个示例都展示了容易注意到的测试代码重复。相比之下,当跨不同测试用例类(第 373页) 中的测试方法(第 348页) 发生重复时,识别重复更具挑战性。

Both of the preceding examples exhibit Test Code Duplication that is easily noticed. By comparison, it is more challenging to identify duplication when it occurs across Test Methods (page 348) that reside in different Testcase Classes (page 373).

影响

Impact

“剪切和粘贴”通常会导致相同代码的多个副本。每次修改 SUT 时,都必须维护此代码,因为修改的方式会影响其方法的语义(例如,参数数量、参数属性、返回的对象属性、调用序列)。这种必要性会大大增加引入新功能的成本(高测试维护成本参见第 265页),因为需要更新包含受影响代码副本的所有测试。

"Cut and paste" often results in many copies of the same code. This code must be maintained every time the SUT is modified in a way that affects the semantics (e.g., number of arguments, argument attributes, returned object attributes, calling sequences) of its methods. This necessity can greatly increase the cost to introduce new functionality (High Test Maintenance Cost; see page 265) because of the effort involved in updating all tests that contain copies of the affected code.

原因

Causes

原因:复制粘贴代码重用

“剪切和粘贴”是一种快速编写代码的强大工具,但它会产生相同代码的多个副本,每个副本都必须并行维护。

"Cut and paste" is a powerful tool for writing code fast but it results in many copies of the same code, each of which must be maintained in parallel.

根本原因

剪切粘贴代码重用通常是重用逻辑的默认方式。专注于“如何”做某事的细节的开发人员通常会多次重复相同的代码,因为他们无法(或不愿意花时间)关注测试的全局(意图)。

Cut-and-Paste Code Reuse is often the default way to reuse logic. Developers who focus on details of "how" to do something will often repeat the same code many times because they cannot (or do not take the time to) focus on the big picture (the intent) of the test.

一个因素可能是缺乏重构技能或重构经验,导致开发人员无法从他们编写的详细代码中提取出全局信息。当然,时间压力也可能是导致重构无法进行的罪魁祸首。结果,测试代码随着时间的推移变得越来越复杂,而不是变得更简单。

A contributing factor may be a lack of refactoring skills or refactoring experience that keeps developers from extracting the big picture from the detailed code they have written. Of course, time pressure may also be the culprit that keeps the refactoring from occurring. As a result, test code grows more complicated over time rather than becoming simpler.

可能的解决方案

一旦发生测试代码重复,最好的解决方案是使用提取方法 [Fowler] 重构从其中一个示例中创建测试实用程序方法(第 599页),然后泛化该方法以处理每个副本。当测试代码重复包含夹具设置逻辑时,我们最终会得到创建方法(第415页) 或查找器方法(请参阅测试实用程序方法)。当逻辑执行结果验证时,我们最终会得到自定义断言(第 474页) 或验证方法(请参阅自定义断言)。

Once Test Code Duplication has occurred, the best solution is to use an Extract Method [Fowler] refactoring to create a Test Utility Method (page 599) from one of the examples and then to generalize that method to handle each of the copies. When the Test Code Duplication consists of fixture setup logic, we end up with Creation Methods (page 415) or Finder Methods (see Test Utility Method). When the logic carries out result verification, we end up with Custom Assertions (page 474) or Verification Methods (see Custom Assertion).

我们可以使用引入参数[JBrains]重构将提取方法中的任何文字常量转换为可传入的参数,以针对调用它的每个测试定制方法的行为。

We can use an Introduce Parameter [JBrains] refactoring to convert any literal constants inside the extracted method into parameters that can be passed in to customize the method's behavior for each test that calls it.

更简单地说,我们可以通过以“由外而内”的方式编写测试方法并专注于其意图来避免大多数测试代码重复。每当我们需要执行涉及多行代码的操作时,我们只需调用不存在的测试实用程序方法即可。我们以这种方式编写所有测试,然后填写测试实用程序方法的实现以使测试编译和运行。(现代 IDE 通过单击鼠标即可提供自动方法骨架生成来简化此过程。)

More simply, we can avoid most Test Code Duplication by writing the Test Methods in an "outside-in" manner, focusing on their intent. Whenever we need to do something that involves several lines of code, we simply call a nonexistent Test Utility Method to do it. We write all our tests this way and then fill in implementations of the Test Utility Methods to get the tests to compile and run. (Modern IDEs facilitate this process by providing automatic method skeleton generation at a click of the mouse.)

原因:重新发明轮子

虽然剪切粘贴代码重用会刻意复制现有代码以减少编写测试的工作量,但也有可能在不同的测试中意外地写入相同的语句序列。

While Cut-and-Paste Code Reuse deliberately makes copies of existing code to reduce the effort of writing tests, it is also possible to accidentally write the same sequence of statements in different tests.

根本原因

这个问题主要是因为缺乏对可用测试实用程序方法的认识。也可能是因为倾向于编写自己的代码而不是重用他人编写的代码。

This problem is primarily caused by a lack of awareness of which Test Utility Methods are available. It can also be caused by a predisposition to write one's own code rather than reuse code written by others.

可能的解决方案

技术解决方案与剪切粘贴代码重用大致相同,但流程解决方案略有不同。测试自动化人员必须查看更多地方以发现哪些测试实用方法可用,否则将重新发明轮子(即编写新代码)。

The technical solution is largely the same as for Cut-and-Paste Code Reuse but the process solution is somewhat different. The test automater must look around more places to discover which Test Utility Methods are available before reinventing the wheel (i.e., writing new code).

进一步阅读

测试代码重复最早在 XP2001 的一篇名为“重构测试代码”[RTC]

Test Code Duplication was first described in a paper at XP2001 called "Refactoring Test Code" [RTC].

生产中的测试逻辑

Test Logic in Production

投入生产的代码包含只应在测试期间执行的逻辑。

The code that is put into production contains logic that should be exercised only during tests.

SUT 可能包含无法在测试环境中运行的逻辑。测试可能要求 SUT 以特定方式运行,以实现全面的测试覆盖。

The SUT may contain logic that cannot be run in a test environment. Tests may require the SUT to behave in specific ways to allow full test coverage.

症状

Symptoms

SUT 中的逻辑仅用于支持测试。此逻辑可能是测试需要的“额外内容”,用于访问 SUT 的内部状态以进行设备设置或结果验证。它也可能包括系统逻辑在检测到正在测试时所经历的更改。

The logic in the SUT is there solely to support testing. This logic may be "extra stuff" that the tests require to gain access to the SUT's internal state for fixture setup or result verification purposes. It may also consist of changes that the logic of the system undergoes when it detects that it is being tested.

影响

Impact

我们不希望在生产中使用测试逻辑,因为它会使 SUT 更加复杂,并为我们想要避免的其他类型的错误打开大门。如果系统在测试实验室中的行为方式是一种,而在生产中的行为方式则完全不同,那么这绝对是灾难的根源!

We would prefer not to end up with Test Logic in Production, as it can make the SUT more complex and opens the door to additional kinds of bugs that we would like to avoid. A system that behaves one way in the test lab and an entirely different way in production is a recipe for disaster!

原因

Causes

原因:测试钩

SUT 内的条件逻辑决定是否运行“真实”代码或特定于测试的逻辑。

Conditional logic within the SUT determines whether the "real" code or test-specific logic is run.

症状

有了这种代码异味,要么没有行为症状,要么生产中可能出现问题。我们可能会在 SUT 中看到类似这样的代码片段:

With this code smell, either there may be no behavioral symptoms or something may go wrong in production. We may see snippets of code in the SUT that look something like this:

if (testing) {

    return hardCodedCannedData;

} else { // 真实逻辑 ...

    return gatherData;

}

if  (testing)  {

    return  hardCodedCannedData;

}  else  {  //  the  real  logic  ...

    return  gatheredData;

}

 

阿丽亚娜

阿丽亚娜 5 号火箭的首飞是一场彻底的灾难:火箭在起飞后仅 37 秒就爆炸了。罪魁祸首是一段看似无害的代码,这段代码仅在火箭在地面时使用,但不幸的是,在飞行的前 40 秒内一直在运行。当它试图将一个代表火箭侧向速度的 64 位数字分配给一个 16 位字段时,导航计算机认为火箭飞错了方向!它试图纠正航向,但方向的突然改变使助推火箭解体。虽然这不完全是生产中的测试逻辑(第 217页) 的一个例子,但它确实说明了与此类错误相关的风险。

通过使用自动化测试,是否可以避免这一灾难?虽然很难肯定地说,而且人们可以肯定地说,任何数量的流程变化都可以在问题发生之前检测到它,但可以想象,自动化测试可以避免这场灾难。

具体来说,测试应该解决边界条件问题,即当数字超过可存储的最大值时会发生什么。这样的测试可以防止生产中第一次出现异常。

此外,阿丽亚娜 4 型火箭的测试记录了最大射程速度。这些测试很可能在开发阿丽亚娜 5 型软件时进行了更新,而新测试可能会因为新火箭速度更快而失败。

有关“这个小虫子”的更详细(且非常有趣)的描述,请访问http://www.around.com/ariane.html



Ariane

The maiden flight of the Ariane 5 rocket was a complete disaster: The rocket blew up only 37 seconds after takeoff. The culprit was a seemingly innocuous bit of code that was used only while the rocket was on the ground but unfortunately was left running for the first 40 seconds of flight. When it tried to assign a 64-bit number representing the sideways velocity of the rocket to a 16-bit field, the navigation computer decided that the rocket was going the wrong way! It tried to correct the course, but the sudden change in direction tore the booster rocket apart. While this is not quite an example of Test Logic in Production (page 217), it certainly does illustrate the risks associated with this type of error.

Could this disaster have been prevented by use of automated tests? While it is difficult to say with certainty, and one could certainly claim that any number of process changes could have detected this problem before it occurred, it is conceivable that automated tests could have averted this catastrophe.

In particular, a test should have addressed the boundary condition—namely, what happens when a number exceeds the maximum value storable. Such a test would have prevented an exception from occurring for the first time ever in production.

In addition, the presence of the tests from the Ariane 4 version of the rocket would have documented the maximum down-range velocity. It is quite possible that these tests would have been updated when the Ariane 5 software was being developed and that the new tests would have failed because of the new rocket's higher speed.

For a slightly more detailed (and very interesting) description of "the little bug that could," visit http://www.around.com/ariane.html.


 
影响

那些不是为在生产环境中运行而设计的代码以及那些没有经过验证可以在生产环境中正常运行的代码可能会意外地在生产中运行并造成严重的问题。

Code that was not designed to work in production and that has not been verified to work properly in the production environment could accidentally be run in production and create serious problems.

阿丽亚娜 5 号火箭在首飞后 37 秒爆炸,原因是一段仅在火箭在地面时使用的代码在飞行的前 40 秒内一直运行。这段代码试图将一个代表火箭横向速度的 64 位数字分配给一个 16 位字段——这一操作使火箭的导航计算机误以为它走错了方向。(有关详细信息,请参阅第 218页关于阿丽亚娜的侧栏。)虽然我们相信Test Hook永远不会在生产中使用,但我们真的想冒这种风险吗?

The Ariane 5 rocket blew up 37 seconds after takeoff on its maiden flight because a piece of code that was used only while the rocket was on the ground was left running for the first 40 seconds of flight. This code tried to assign a 64-bit number representing the sideways velocity of the rocket to a 16-bit field—an operation that convinced the rocket's navigation computer that it was going the wrong way. (See the sidebar on Ariane on page 218 for more details.) While we believe the Test Hook would never be exercised in production, do we really want to take this kind of chance?

根本原因

在某些情况下,引入生产中的测试逻辑是为了通过返回已知(硬编码)值来使 SUT 的行为更加确定。在其他情况下,引入生产中的测试逻辑可能是为了避免执行无法在测试环境中运行的代码。不幸的是,如果配置错误,这种方法可能会导致无法在生产环境中执行该代码。

In some cases, the Test Logic in Production is introduced to make the behavior of the SUT more deterministic by returning known (hard-coded) values. In other cases, the Test Logic in Production may have been introduced to avoid executing code that cannot be run in a test environment. Unfortunately, this approach can result in failure to execute that code in the production environment if something is misconfigured.

在某些情况下,测试可能要求 SUT 执行额外的代码,而这些代码原本会由依赖的组件执行。例如,如果数据库被假数据库替换(请参阅第 551页的假对象),则从数据库中的触发器运行的代码将不会运行;因此,测试需要确保从 SUT 中的某个位置执行等效逻辑。

In some cases, tests may require that the SUT execute additional code that would otherwise be executed by a depended-on component. For example, code run from a trigger in a database will not run if the database is replaced by a Fake Database (see Fake Object on page 551); thus the test needs to ensure that the equivalent logic is executed from somewhere within the SUT.

可能的解决方案

我们可以将测试逻辑移到可替代的依赖项中,而不是直接将测试逻辑添加到生产代码中。我们可以将只在生产中运行的代码放入默认安装的Strategy [GOF]对象中,并在运行测试时将其替换为 Null Object [PLOPD3]。相反,可以将只在测试期间运行的代码放入默认配置为 Null Object 的 Strategy [GOF]对象中。然后,当我们希望 SUT 在测试期间执行额外代码时,我们可以用特定于测试的版本替换此 Strategy 对象。为确保正确配置此机制,我们应该进行构造函数测试(请参阅第348页的测试方法)以验证当测试未覆盖任何保存对 Strategy 对象的引用的变量时,这些变量是否已正确初始化。

Instead of adding test logic into the production code directly, we can move logic into a substitutable dependency. We can put code that should be run in only production into a Strategy [GOF] object that is installed by default and replaced by a Null Object [PLOPD3] when running our tests. In contrast, code that should be run only during tests can be put into a Strategy [GOF] object that is configured as a Null Object by default. Then, when we want the SUT to execute extra code during testing, we can replace this Strategy object with a test-specific version. To ensure this mechanism is configured properly, we should have a Constructor Test (see Test Method on page 348) to verify that any variables holding references to Strategy objects are initialized correctly when they are not overridden by the test.

如果我们想要规避的生产逻辑位于可覆盖方法中,那么也可以在测试特定子类(第 579页) 中覆盖 SUT 的特定方法。此功能由自调用[WWW]启用。

It may also be possible to override specific methods of the SUT in a Test-Specific Subclass (page 579) if the production logic we want to circumvent is localized in overridable methods. This ability is enabled by Self-Calls [WWW].

原因:仅用于测试

代码存在于 SUT 中,严格供测试使用。

Code exists in the SUT strictly for use by tests.

症状

SUT 的某些方法仅供测试使用。某些属性是公开的,但实际上它们应该是私有的。

Some of the methods of the SUT are used only by tests. Some of the attributes are public when they really should be private.

影响

添加到 SUT仅用于测试的软件会使 SUT 更加复杂。它可能会通过引入除测试之外的任何代码都不应使用的额外方法,使软件界面的潜在客户感到困惑。这些方法可能仅在非常特殊的情况下进行了测试,因此它们可能无法在实际客户端软件使用的典型使用模式下工作。

Software that is added to the SUT For Tests Only makes the SUT more complex. It can confuse potential clients of the software's interface by introducing additional methods that should not be used by any code other than the tests. These methods may have been tested only in very specific circumstances, so they might not work in the typical usage patterns used by real client software.

根本原因

测试自动化人员可能需要向类添加方法来公开测试所需的信息,或者提供对初始化的更大控制的方法(例如安装测试替身;参见第522页)。测试驱动开发将导致创建这些额外的方法,即使客户端实际上并不需要它们。在将测试改造到遗留代码上时,测试自动化人员可能需要访问尚未公开的信息或功能。

The test automater may need to add methods to a class that expose information needed by the test or methods that provide greater control over initialization (such as for the installation of a Test Double; see page 522). Test-driven development will lead to the creation of these additional methods even though they aren't really needed by clients. When retrofitting tests onto legacy code, the test automater may need access to information or functionality that is not already exposed.

当 SUT 在现实生活中不对称使用时,也会导致仅用于测试。自动化测试(尤其是往返测试)通常以更对称的方式使用软件,因此可能需要实际软件客户端不需要的方法。

For Tests Only can also result when a SUT is used asymmetrically in real life. Automated tests (especially round-trip tests) typically use software in a more symmetric fashion and hence may need methods that the real software clients do not need.

可能的解决方案

我们可以通过创建 SUT 的测试专用子类来确保测试能够访问私有信息,然后该子类会提供方法来公开所需的属性或初始化逻辑。要使此方法有效,测试需要能够创建子类(而不是 SUT 类)的实例。

We can assure that tests have access to private information by creating a Test-Specific Subclass of the SUT, which then provides methods to expose the needed attributes or initialization logic. A test needs to be able to create instances of the subclass instead of the SUT class for this approach to work.

如果由于某种原因,额外的方法不能移到特定于测试的子类中,则应明确标记为仅用于测试。这可以通过采用命名约定来实现,例如以“FTO_”开头的名称。

If for some reason the extra methods cannot be moved to a Test-Specific Subclass, they should be clearly labeled For Tests Only. This can be done by adopting a naming convention such as starting the names with "FTO_".

原因:生产中的测试依赖性

生产可执行文件依赖于测试可执行文件。

Production executables depend on test executables.

症状

我们不能只构建生产代码;构建中必须包含一些测试代码才能编译生产代码。或者,我们可能会注意到,如果没有测试可执行文件,我们就无法运行生产代码。

We cannot build only the production code; some test code must be included in the build to allow the production code to compile. Alternatively, we might notice that we cannot run the production code if the test executables are not present.

影响

即使生产模块不包含任何测试代码,如果这些模块中的任何一个依赖于测试模块,也会出现问题。即使生产场景中没有实际使用任何测试代码,这种依赖关系至少会增加可执行文件的大小。它还为在生产过程中意外执行测试代码打开了大门。

Even if the production modules do not contain any test code, problems can arise if any of these modules depends on a test module. At minimum, this dependency increases the size of the executable even if none of the test code is actually used in production scenarios. It also opens the door to accidental execution of test code during production.

根本原因

生产中的测试依赖性通常是由于对模块间依赖性关注不足而导致的。当内置自测试需要访问测试自动化基础架构的某些部分(例如测试实用程序方法第 599页)或测试自动化框架第 298页) )以报告测试结果时,也可能会出现这种情况。

Test Dependency in Production is usually caused by a lack of attention to inter-module dependencies. It may also arise when a built-in self-test requires access to parts of the test automation infrastructure, such as Test Utility Methods (page 599) or the Test Automation Framework (page 298), to report test results.

可能的解决方案

我们必须谨慎管理我们的依赖关系,以确保生产代码不会依赖于测试代码,即使是类型定义等无害的东西。

We must manage our dependencies carefully to ensure that no production code depends on test code even for innocuous things such as type definitions.

测试和生产代码所需的任何内容都应位于两者都可访问的生产模块或类中。

Anything required by both test and production code should live in a production module or class that is accessible to both.

原因:平等污染

生产中的测试逻辑的另一个原因是SUT 方法中测试特定相等性的实现。equals

Another cause of Test Logic in Production is the implementation of test-specific equality in the equals method of the SUT.

症状

一旦发生平等污染equals,就很难发现——值得注意的是,SUT 实际上并不需要实现该方法。在其他情况下,可能会出现行为症状,例如,当equals修改方法以支持测试的特定需求时,或者当 SUT 中的变化定义equals作为新功能或用户故事的一部分

Equality Pollution can be difficult to spot once it has occurred—what is notable is that the SUT doesn't actually need the equals method to be implemented. In other cases, behavioral symptoms may appear, such as test failure when the equals method is modified to support the specific needs of a test or when the definition of equals changes within the SUT as part of a new feature or user story.

影响

我们可能会为了满足测试而编写不必要的equals方法。我们也可能会更改定义,equals以致于它不再满足业务需求。

We may write unnecessary equals methods simply to satisfy tests. We may also change the definition of equals so that it no longer satisfies the business requirements.

equals如果已经存在支持另一项测试的特定测试相等性的逻辑,那么相等性污染可能会使引入某些新要求规定的逻辑变得困难

Equality Pollution may make it difficult to introduce the equals logic prescribed by some new requirement if it already exists to support test-specific equality for another test.

根本原因

平等污染是由于缺乏对测试特定平等概念的认识而导致的。一些早期版本的动态Mock Object (page 544 ) 生成工具强迫我们使用 SUT 的定义equals,从而导致了平等污染

Equality Pollution is caused by a lack of awareness of the concept of test-specific equality. Some early versions of dynamic Mock Object (page 544) generation tools forced us to use the SUT's definition of equals, which led to Equality Pollution.

可能的解决方案

当测试需要测试特定的相等性时,我们应该使用自定义断言第 474页),而不是修改equals方法,以便我们可以使用内置相等性断言(请参阅第 362页的断言方法)。

When a test requires test-specific equality, we should use a Custom Assertion (page 474) instead of modifying the equals method just so that we can use a built-in Equality Assertion (see Assertion Method on page 362).

使用动态Mock 对象生成工具时,我们应该使用比较器[WWW],而不是依赖equalsSUT 提供的方法。我们还可以在预期对象测试特定子类equals上实现该方法(请参阅第462页的状态验证),以避免将其直接添加到生产类中。

When using dynamic Mock Object generation tools, we should use a Comparator [WWW] rather than relying on the equals method supplied by the SUT. We can also implement the equals method on a Test-Specific Subclass of an Expected Object (see State Verification on page 462) to avoid adding it to a production class directly.

进一步阅读

仅用于测试平等污染首次出现在 XP2001 的一篇名为“重构测试代码” [RTC]的论文中。

For Tests Only and Equality Pollution were first introduced in a paper at XP2001 called "Refactoring Test Code" [RTC].

第 16 章

行为气味

Chapter 16

Behavior Smells

 

本章中的气味

Smells in This Chapter

      

断言轮盘 224

      

Assertion Roulette 224

      

不稳定测试 228

      

Erratic Test 228

      

易碎测试 239

      

Fragile Test 239

      

频繁调试 248

      

Frequent Debugging 248

      

人工干预 250

      

Manual Intervention 250

      

慢速测试253

      

Slow Tests 253

断言轮盘

Assertion Roulette

很难判断同一测试方法中的哪几个断言导致了测试失败。

It is hard to tell which of several assertions within the same test method caused a test failure.

症状

Symptoms

测试失败。检查测试运行器(第 377页) 的输出后,我们无法确定到底是哪个断言失败了。

A test fails. Upon examining the output of the Test Runner (page 377), we cannot determine exactly which assertion failed.

影响

Impact

当自动化集成构建 [SCM] 期间测试失败时,可能很难准确判断哪个断言失败。如果问题无法在开发人员的机器上重现(如果问题是由环境问题或资源乐观引起的,则可能如此;请参阅第 228页的不稳定测试),解决问题可能很困难且耗时。

When a test fails during an automated Integration Build [SCM], it may be hard to tell exactly which assertion failed. If the problem cannot be reproduced on a developer's machine (as may be the case if the problem is caused by environmental issues or Resource Optimism; see Erratic Test on page 228) fixing the problem may be difficult and time-consuming.

原因

Causes

原因:急切测试

单一测试验证了太多功能。

A single test verifies too much functionality.

症状

测试会练习 SUT 的几种方法,或者调用相同的方法几次,其中穿插着夹具设置逻辑和断言。

A test exercises several methods of the SUT or calls the same method several times interspersed with fixture setup logic and assertions.

public void testFlightMileage_asKm2() throws Exception {

      // 设置夹具

      // 练习构造函数

      Flight newFlight = new Flight(validFlightNumber);

      // 验证构造的对象

      assertEquals(validFlightNumber, newFlight.number);

      assertEquals("", newFlight.airlineCode);

      assertNull(newFlight.airline);

      // 设置里程

      newFlight.setMileage(1122);

      // 练习里程转换器

      int actualKilometres = newFlight.getMileageAsKm();

      // 验证结果

      int expectedKilometres = 1810;

      assertEquals( expectedKilometres, actualKilometres);

      // 现在用取消的航班尝试

      newFlight.cancel();

      try {

          newFlight.getMileageAsKm();

          fail("Expected exception");

      } catch (InvalidRequestException e) {

          assertEquals( "无法获取取消的航班里程",

                              e.getMessage());

      }

}

public  void  testFlightMileage_asKm2()  throws  Exception  {

      //  set  up  fixture

      //  exercise  constructor

      Flight  newFlight  =  new  Flight(validFlightNumber);

      //  verify  constructed  object

      assertEquals(validFlightNumber,  newFlight.number);

      assertEquals("",  newFlight.airlineCode);

      assertNull(newFlight.airline);

      //  set  up  mileage

      newFlight.setMileage(1122);

      //  exercise  mileage  translator

      int  actualKilometres  =  newFlight.getMileageAsKm();

      //  verify  results

      int  expectedKilometres  =  1810;

      assertEquals(  expectedKilometres,  actualKilometres);

      //  now  try  it  with  a  canceled  flight

      newFlight.cancel();

      try  {

          newFlight.getMileageAsKm();

          fail("Expected  exception");

      }  catch  (InvalidRequestException  e)  {

          assertEquals(  "Cannot  get  cancelled  flight  mileage",

                              e.getMessage());

      }

}

 

另一个可能的症状是,测试自动化程序想要修改测试自动化框架第 298页),以便在断言失败后继续运行,以便可以执行其余的断言。

Another possible symptom is that the test automater wants to modify the Test Automation Framework (page 298) to keep going after an assertion has failed so that the rest of the assertions can be executed.

根本原因

急切测试通常是由于试图通过在单个测试方法(第 348 页)中验证许多测试条件来尽量减少单元测试的数量 (无论是有意还是无意) 而引起的。虽然这对于手动执行的测试来说是一种很好的做法,因为“实时软件”会解释结果并实时调整测试,但对于全自动测试(参见第26页) 来说,它效果并不好。

An Eager Test is often caused by trying to minimize the number of unit tests (whether consciously or unconsciously) by verifying many test conditions in a single Test Method (page 348). While this is a good practice for manually executed tests that have "liveware" interpreting the results and adjusting the tests in real time, it just doesn't work very well for Fully Automated Tests (see page 26).

另一个导致急切测试的常见原因是使用 xUnit 来自动化需要很多步骤的客户测试,从而在每次测试中验证 SUT 的许多方面。这些测试必然比单元测试长,但应注意使其尽可能短(但不能更短!)。

Another common cause of Eager Tests is using xUnit to automate customer tests that require many steps, thereby verifying many aspects of the SUT in each test. These tests are necessarily longer than unit tests but care should be taken to keep them as short as possible (but no shorter!).

可能的解决方案

对于单元测试,我们通过拆分Eager Test,将测试分解为一系列单条件测试(参见第45页) 。可以使用一个或多个 Extract Method [Fowler] 重构将独立的部分提取到它们自己的Test Methods中来实现这一点。有时,为每个测试条件克隆一次测试,然后通过删除该特定测试条件不需要的任何代码来清理每个测试方法会更容易。设置 Fixture 或将 SUT 置于正确的起始状态所需的任何代码都可以提取到Creation Method(第415页)中。然后,好的 IDE 或编译器将帮助我们确定哪些变量不再使用。

For unit tests, we break up the test into a suite of Single-Condition Tests (see page 45) by teasing apart the Eager Test. It may be possible to do so by using one or more Extract Method [Fowler] refactorings to pull out independent pieces into their own Test Methods. Sometimes it is easier to clone the test once for each test condition and then clean up each Test Method by removing any code that is not required for that particular test conditions. Any code required to set up the fixture or put the SUT into the correct starting state can be extracted into a Creation Method (page 415). A good IDE or compiler will then help us determine which variables are no longer being used.

如果我们使用 xUnit 自动化客户测试,并且由于工作流程需要复杂的夹具设置,这项工作导致每个测试都包含许多步骤,那么我们可以考虑使用其他方式为测试的后半部分设置夹具。如果我们可以使用后门设置(请参阅第327页的后门操作)独立于第一部分创建测试最后一部分的夹具,我们可以将一个测试分成两个,从而改善我们的缺陷定位(请参阅测试自动化的目标)。我们应该重复这个过程,直到测试足够短,一目了然,并清楚地传达意图(请参阅第41页)。

If we are automating customer tests using xUnit, and this effort has resulted in many steps in each test because the work flows require complex fixture setup, we could consider using some other way to set up the fixture for the latter parts of the test. If we can use Back Door Setup (see Back Door Manipulation on page 327) to create the fixture for the last part of the test independently of the first part, we can break one test into two, thereby improving our Defect Localization (see Goals of Test Automation). We should repeat this process as many times as it takes to make the tests short enough to be readable at a single glance and to Communicate Intent (see page 41) clearly.

原因:缺少断言消息
症状

测试失败。检查测试运行器的输出后,我们无法确定到底是哪个断言失败了。

A test fails. Upon examining the output of the Test Runner, we cannot determine exactly which assertion failed.

根本原因

此问题是由于使用了与断言消息(第 370页) 相同或缺失的断言方法(第 362页) 调用而导致的。在使用命令行测试运行器(请参阅测试运行器) 或未与程序文本编辑器或开发环境集成的测试运行器运行测试时最常遇到此问题。

This problem is caused by the use of Assertion Method (page 362) calls with identical or missing Assertion Messages (page 370). It is most commonly encountered when running tests using a Command-Line Test Runner (see Test Runner) or a Test Runner that is not integrated with the program text editor or development environment.

在下面的测试中,我们有许多相等断言(参见断言方法):

In the following test, we have a number of Equality Assertions (see Assertion Method):

public void testInvoice_addLineItem7() {

      LineItem expItem = new LineItem(inv, product, QUANTITY);

      // 练习

      inv.addItemQuantity(product, QUANTITY);

      // 验证

      列表 lineItems = inv.getLineItems();

      LineItem actual = (LineItem)lineItems.get(0);

      assertEquals(expItem.getInv(), actual.getInv());

      assertEquals(expItem.getProd(), actual.getProd());

      assertEquals(expItem.getQuantity(), actual.getQuantity());

}

public  void  testInvoice_addLineItem7()  {

      LineItem  expItem  =  new  LineItem(inv,  product,  QUANTITY);

      //  Exercise

      inv.addItemQuantity(product,  QUANTITY);

      //  Verify

      List  lineItems  =  inv.getLineItems();

      LineItem  actual  =  (LineItem)lineItems.get(0);

      assertEquals(expItem.getInv(),  actual.getInv());

      assertEquals(expItem.getProd(),  actual.getProd());

      assertEquals(expItem.getQuantity(),  actual.getQuantity());

}

 

当断言失败时,我们会知道是哪一个吗?相等断言通常会打印出预期值和实际值 - 但如果预期值相似或打印出来很模糊,则可能很难判断哪个断言失败了。一个好的经验法则是,每当我们对同一种断言方法进行多次调用时,至少包含一个最小的断言消息

When an assertion fails, will we know which one it was? An Equality Assertion typically prints out both the expected and the actual values—but it may prove difficult to tell which assertion failed if the expected values are similar or print out cryptically. A good rule of thumb is to include at least a minimal Assertion Message whenever we have more than one call to the same kind of Assertion Method.

可能的解决方案

如果问题发生在我们使用集成了 IDE 的图形测试运行器(参见测试运行器)运行测试时,我们应该能够点击堆栈回溯中的相应行,让 IDE 突出显示失败的断言。如果失败,我们可以打开调试器并单步执行测试以查看哪个断言语句失败。

If the problem occurred while we were running a test using a Graphical Test Runner (see Test Runner) with IDE integration, we should be able to click on the appropriate line in the stack traceback to have the IDE highlight the failed assertion. Failing this, we can turn on the debugger and single-step through the test to see which assertion statement fails.

如果问题发生在我们使用命令行测试运行器运行测试时,我们可以尝试从集成了 IDE 的图形测试运行器运行测试,以确定有问题的断言。如果这不起作用,我们可能不得不求助于使用行号(如果可用)或应用排除法来推断哪些断言不可能,以缩小可能性。当然,我们可以硬着头皮在每次调用断言方法时添加一个唯一的断言消息(甚至只是一个数字!) 。

If the problem occurred while we were running a test using a Command-Line Test Runner, we can try running the test from a Graphical Test Runner with IDE integration to determine the offending assertion. If that doesn't work, we may have to resort to using line numbers (if available) or apply a process of elimination to deduce which of the assertions it couldn't be to narrow down the possibilities. Of course, we could just bite the bullet and add a unique Assertion Message (even just a number!) to each call to an Assertion Method.

进一步阅读

断言轮盘Eager 测试最早是在 XP2001 的一篇名为“重构测试代码”[RTC]

Assertion Roulette and Eager Test were first described in a paper presented at XP2001 called "Refactoring Test Code" [RTC].

不稳定的测试

Erratic Test

一个或多个测试行为不稳定;有时会通过,有时会失败。

One or more tests behave erratically; sometimes they pass and sometimes they fail.

症状

Symptoms

我们有一个或多个测试在运行,但根据运行时间和运行人员的不同,结果也不同。在某些情况下,当由一位开发人员运行不稳定测试时,结果始终相同,但由其他人或在不同环境中运行时,结果会失败。在其他情况下,当从同一个测试运行器(第 377页)运行不稳定测试时,结果会不同。

We have one or more tests that run but give different results depending on when they are run and who is running them. In some cases, the Erratic Test will consistently give the same results when run by one developer but fail when run by someone else or in a different environment. In other cases, the Erratic Test will give different results when run from the same Test Runner (page 377).

影响

Impact

我们可能会试图将失败的测试从套件中删除,以“保持条形图为绿色”,但这会导致(故意的)丢失测试(请参阅第268页的生产错误)。如果我们选择将不稳定的测试保留在测试套件中,尽管失败了,已知的失败可能会掩盖其他问题,例如由相同测试检测到的另一个问题。仅仅有一个测试失败可能会导致我们错过其他失败,因为看到从绿条红条的变化比注意到两个测试失败而不是我们预期的只有一个测试失败要容易得多。

We may be tempted to remove the failing test from the suite to "keep the bar green" but this would result in an (intentional) Lost Test (see Production Bugs on page 268). If we choose to keep the Erratic Test in the test suite despite the failures, the known failure may obscure other problems, such as another issue detected by the same tests. Just having a test fail can cause us to miss additional failures because it is much easier to see the change from a green bar to a red bar than to notice that two tests are failing instead of just the one we expected.

故障排除建议

Troubleshooting Advice

不稳定测试的故障排除可能具有挑战性,因为存在许多潜在原因。如果无法轻易确定原因,则可能需要在一段时间内系统地收集数据。测试在哪里(在哪些环境中)通过,在哪里失败?所有测试都在运行还是只运行其中的一部分?当测试套件连续运行几次时,行为是否发生了任何变化?当从多个测试运行器同时运行时,行为是否发生了任何变化?

Erratic Tests can be challenging to troubleshoot because so many potential causes exist. If the cause cannot be easily determined, it may be necessary to collect data systematically over a period of time. Where (in which environments) did the tests pass, and where did they fail? Were all the tests being run or just a subset of them? Did any change in behavior occur when the test suite was run several times in a row? Did any change in behavior occur when it was run from several Test Runners at the same time?

一旦我们有了一些数据,就应该更容易将观察到的症状与每个潜在原因列出的症状相匹配,并将可能性列表缩小到少数几个候选者。然后我们可以收集更多数据,重点关注可能原因之间的症状差异。图 16.1总结了确定我们正在处理的哪个不稳定测试原因的过程。

Once we have some data, it should be easier to match up the observed symptoms with those listed for each of the potential causes and to narrow the list of possibilities to a handful of candidates. Then we can collect some more data focusing on differences in symptoms between the possible causes. Figure 16.1 summarizes the process for determining which cause of an Erratic Test we are dealing with.

图 16.1. 对不稳定测试进行故障排除。

Figure 16.1. Troubleshooting an Erratic Test.

图像

原因

Causes

测试可能会因多种原因而出现异常。通常可以通过持续的侦查来确定根本原因,方法是关注测试失败的方式和时间模式。有些原因很常见,值得给出名称和纠正它们的具体建议。

Tests may behave erratically for a number of reasons. The underlying cause can usually be determined through some persistent sleuthing by paying attention to patterns regarding how and when the tests fail. Some of the causes are common enough to warrant giving them names and specific advice for rectifying them.

原因:交互测试

测试在某种程度上依赖于其他测试。请注意,交互测试套件Lonely Test交互测试的特定变体。

Tests depend on other tests in some way. Note that Interacting Test Suites and Lonely Test are specific variations of Interacting Tests.

症状

一个可以自行运行的测试在下列情况下会突然失败:

A test that works by itself suddenly fails in the following circumstances:

  • 另一个测试被添加到套件中(或从套件中删除)。
  • Another test is added to (or removed from) the suite.
  • 套件中的另一个测试失败(或开始通过)。
  • Another test in the suite fails (or starts to pass).
  • 该测试(或另一个测试)在源文件中被重命名或移动。
  • The test (or another test) is renamed or moved in the source file.
  • 已安装新版本的Test Runner 。
  • A new version of the Test Runner is installed.
根本原因

交互测试通常发生在测试使用共享装置(第 317页) 时,其中一个测试以某种方式依赖于另一个测试的结果。交互测试的原因可以从两个角度描述:

Interacting Tests usually arise when tests use a Shared Fixture (page 317), with one test depending in some way on the outcome of another test. The cause of Interacting Tests can be described from two perspectives:

  • 相互作用机制
  • The mechanism of interaction
  • 互动原因
  • The reason for interaction

交互机制可能非常明显(例如,测试包含数据库的 SUT),也可能更加隐蔽。任何超过测试生命周期的东西都可能导致交互;静态变量可能被依赖来导致交互测试,因此在 SUT 和测试自动化框架第 298页)中都应避免使用静态变量!请参阅第 384页的侧栏“总是有异常”以了解后一个问题的示例。单例[GOF]和注册表[PEAA]是应尽可能避免在 SUT 中出现的问题的很好例子。如果必须使用它们,最好包含一种机制,以便在每次测试开始时重新初始化它们的静态变量。

The mechanism for interaction could be something blatantly obvious—for example, testing an SUT that includes a database—or it could be more subtle. Anything that outlives the lifetime of the test can lead to interactions; static variables can be depended on to cause Interacting Tests and, therefore, should be avoided in both the SUT and the Test Automation Framework (page 298)! See the sidebar "There's Always an Exception" on page 384 for an example of the latter problem. Singletons [GOF] and Registries [PEAA] are good examples of things to avoid in the SUT if at all possible. If we must use them, it is best to include a mechanism to reinitialize their static variables at the beginning of each test.

测试可能会因多种原因而相互作用,无论是有意还是无意:

Tests may interact for a number of reasons, either by design or by accident:

  • 依赖于另一个测试的 Fixture 设置阶段所构建的 Fixture
  • Depending on the fixture constructed by the fixture setup phase of another test
  • 根据另一项测试的练习 SUT 阶段对 SUT 所做的更改
  • Depending on the changes made to the SUT during the exercise SUT phase of another test
  • 同一测试运行中两个测试运行之间由于某些互斥操作(可能是上述问题之一)而引起的碰撞
  • A collision caused by some mutually exclusive action (which may be either of the problems mentioned above) between two tests run in the same test run

如果依赖的测试

The dependencies may suddenly cease to be satisfied if the depended-on test

  • 被赶出套房,
  • Is removed from the suite,
  • 修改后不再改变 SUT 的状态,
  • Is modified to no longer change the state of the SUT,
  • 尝试改变 SUT 的状态失败,或者
  • Fails in its attempt to change the state of the SUT, or
  • 在有问题的测试之后运行(因为它被重命名或移动到不同的测试用例类;参见第373页)。
  • Is run after the test in question (because it was renamed or moved to a different Testcase Class; see page 373).

类似地,当碰撞测试

Similarly, collisions may start occurring when the colliding test is

  • 添加到套件中,
  • Added to the suite,
  • 首次通过,或
  • Passes for the first time, or
  • 在依赖测试之前运行。
  • Runs before the dependent test.

在许多情况下,多个测试都会失败。一些测试可能因为一个好的理由而失败——即 SUT 没有做它应该做的事情。依赖测试可能因为错误的原因而失败——因为它们被编码为依赖于其他测试的成功。因此,它们可能给出“假阳性”(假失败)指示。

In many of these cases, multiple tests will fail. Some of the tests may fail for a good reason—namely, the SUT is not doing what it is supposed to do. Dependent tests may fail for the wrong reason—because they were coded to depend on other tests' success. As a result, they may be giving a "false-positive" (false-failure) indication.

总的来说,由于上述问题,依赖测试执行顺序并不是一个明智的方法。xUnit 框架的大多数变体都不保证测试套件中测试执行的顺序。(然而, TestNG通过提供管理依赖项的功能来促进测试之间的相互依赖性。)

In general, depending on the order of test execution is not a wise approach because of the problems described above. Most variants of the xUnit framework do not make any guarantees about the order of test execution within a test suite. (TestNG, however, promotes interdependencies between tests by providing features to manage the dependencies.)

可能的解决方案

使用全新 Fixture第 311页)是交互测试的首选解决方案;它几乎可以保证解决问题。如果必须使用共享 Fixture,则应考虑使用不可变共享 Fixture(请参阅共享 Fixture),通过从头创建它们想要修改的 Fixture 部分,防止测试通过 Fixture 中的更改相互交互。

Using a Fresh Fixture (page 311) is the preferred solution for Interacting Tests; it is almost guaranteed to solve the problem. If we must use a Shared Fixture, we should consider using an Immutable Shared Fixture (see Shared Fixture) to prevent the tests from interacting with one another through changes in the fixture by creating from scratch those parts of the fixture that they intend to modify.

如果由于另一个测试未创建预期的对象或数据库数据而出现未满足的依赖关系,则我们应考虑使用延迟设置第 435页)在两个测试中创建对象或数据。此方法可确保执行的第一个测试为两个测试创建对象或数据。我们可以将夹具设置代码放入创建方法第 415页)中,以避免测试代码重复(第213页)。如果测试在不同的测试用例类上,我们可以将夹具设置代码移动到测试助手第 643页)。

If an unsatisfied dependency arises because another test does not create the expected objects or database data, we should consider using Lazy Setup (page 435) to create the objects or data in both tests. This approach ensures that the first test to execute creates the objects or data for both tests. We can put the fixture setup code into a Creation Method (page 415) to avoid Test Code Duplication (page 213). If the tests are on different Testcase Classes, we can move the fixture setup code to a Test Helper (page 643).

有时,碰撞可能是由我们在测试中创建但之后未清理的对象或数据库数据引起的。在这种情况下,我们应该考虑实施自动夹具拆卸(请参阅第503页的自动拆卸),以安全有效地移除它们。

Sometimes the collision may be caused by objects or database data that are created in our test but not cleaned up afterward. In such a case, we should consider implementing Automated Fixture Teardown (see Automated Teardown on page 503) to remove them safely and efficiently.

找出任何测试是否相互依赖的一种快速方法是以不同于正常顺序的顺序运行测试。例如,以相反的顺序运行整个测试套件就可以很好地解决问题。定期这样做将有助于避免意外引入交互测试

A quick way to find out whether any tests depend on one another is to run the tests in a different order than the normal order. Running the entire test suite in reverse order, for example, would do the trick nicely. Doing so regularly would help avoid accidental introduction of Interacting Tests.

原因:交互测试套件

在这种特殊的交互测试情况下,测试位于不同的测试套件中。

In this special case of Interacting Tests, the tests are in different test suites.

症状

当测试在其自己的测试套件中运行时,测试会通过,但当它在套件的套件中运行时,测试会失败(请参阅第387页的测试套件对象)。

A test passes when it is run in its own test suite but fails when it is run within a Suite of Suites (see Test Suite Object on page 387).

Suite1.run()--> 绿色

Suite2.run()--> 绿色

Suite(Suite1,Suite2).run()--> Suite2 中的测试 C 失败

Suite1.run()-->  Green

Suite2.run()-->  Green

Suite(Suite1,Suite2).run()-->  Test  C  in  Suite2  fails

 
根本原因

交互测试套件通常发生在不同测试套件中的测试尝试创建相同资源时。当它们在同一个套件中运行时,第一个测试套件成功,但第二个测试套件在尝试创建资源时失败。

Interacting Test Suites usually occur when tests in separate test suites try to create the same resource. When they are run in the same suite, the first one succeeds but the second one fails while trying to create the resource.

也许只需查看测试失败或阅读失败的测试方法(第 348页),就能看出问题的性质。如果不能,我们可以尝试从(未失败的)测试套件中逐个删除其他测试。当不再发生失败时,我们只需检查最后删除的测试是否存在可能导致与其他(失败的)测试交互的行为。特别是,我们需要查看可能涉及共享夹具的所有内容,包括初始化类变量的所有位置。这些位置可能是在测试方法本身内,setUp方法内,或在任何调用的测试实用程序方法(第 599页) 中。

The nature of the problem may be obvious just by looking at the test failure or by reading the failed Test Method (page 348). If it is not, we can try removing other tests from the (nonfailing) test suite, one by one. When the failure stops occurring, we simply examine the last test we removed for behaviors that might cause the interactions with the other (failing) test. In particular, we need to look at anything that might involve a Shared Fixture, including all places where class variables are initialized. These locations may be within the Test Method itself, within a setUp method, or in any Test Utility Methods (page 599) that are called.

警告:同一测试套件中可能会有多对测试交互!交互也可能是由多个测试用例类的套件装置设置(第 441页) 或设置装饰器(第447页)冲突引起的,而不是由实际测试方法之间的冲突引起的!

Warning: There may be more than one pair of tests interacting in the same test suite! The interaction may also be caused by the Suite Fixture Setup (page 441) or Setup Decorator (page 447) of several Testcase Classes clashing rather than by a conflict between the actual Test Methods!

使用测试用例类发现(请参阅第 393页的测试发现)的 xUnit 变体(例如 NUnit)可能看似不使用测试套件。实际上,它们确实使用了 — 它们只是不希望测试自动化人员使用测试套件工厂(请参阅第399页的测试枚举)来向测试运行器识别测试套件对象

Variants of xUnit that use Testcase Class Discovery (see Test Discovery on page 393), such as NUnit, may appear to not use test suites. In reality, they do—they just don't expect the test automaters to use a Test Suite Factory (see Test Enumeration on page 399) to identify the Test Suite Object to the Test Runner.

可能的解决方案

当然,我们可以通过使用Fresh Fixture 来完全消除此问题。如果此解决方案不在我们的范围内,我们可以尝试使用Immutable Shared Fixture来阻止测试的交互。

We could, of course, eliminate this problem entirely by using a Fresh Fixture. If this solution isn't within our scope, we could try using an Immutable Shared Fixture to prevent the tests' interaction.

如果问题是由一个测试创建的剩余对象或数据库行引起的,并且与后续测试创建的装置相冲突,我们应该考虑使用自动拆卸以消除编写容易出错的清理代码的需要。

If the problem is caused by leftover objects or database rows created by one test that conflict with the fixture being created by a later test, we should consider using Automated Teardown to eliminate the need to write error-prone cleanup code.

原因:孤独的测试

单独测试是交互测试的一种特殊情况。在这种情况下,测试可以作为套件的一部分运行,但不能单独运行,因为它依赖于由另一个测试(例如,链式测试;参见第454页)或套件级装置设置逻辑(例如,设置装饰器)创建的共享装置中的某些内容。

A Lonely Test is a special case of Interacting Tests. In this case, a test can be run as part of a suite but cannot be run by itself because it depends on something in a Shared Fixture that was created by another test (e.g., Chained Tests; see page 454) or by suite-level fixture setup logic (e.g., a Setup Decorator).

我们可以通过将测试转换为使用Fresh Fixture或向Lonely Test添加Lazy Setup逻辑以允许它自行运行来解决此问题。

We can address this problem by converting the test to use a Fresh Fixture or by adding Lazy Setup logic to the Lonely Test to allow it to run by itself.

原因:资源泄漏

测试或 SUT 消耗有限的资源。

Tests or the SUT consume finite resources.

症状

测试运行速度越来越慢或突然失败。重新初始化测试运行器、 SUT 或数据库沙箱第 650页)可以解决问题,但随着时间的推移,问题又会重新出现。

Tests run more and more slowly or start to fail suddenly. Reinitializing the Test Runner, SUT, or Database Sandbox (page 650) clears up the problem—only to have it reappear over time.

根本原因

测试或 SUT 会分配有限的资源,但之后无法释放这些资源。这种做法可能会使测试运行速度变慢。随着时间的推移,所有资源都会用尽,依赖这些资源的测试就会开始失败。

Tests or the SUT consume finite resources by allocating those resources and failing to free them afterward. This practice may make the tests run more slowly. Over time, all the resources are used up and tests that depend on them start to fail.

该问题可能是由以下两种类型的错误之一引起的:

This problem can be caused by one of two types of bugs:

  • SUT 未能正确清理资源。我们越早发现这种行为,就能越早追踪并修复它。
  • The SUT fails to clean up the resources properly. The sooner we detect this behavior, the sooner we can track it down and fix it.
  • 测试本身在装置设置过程中分配资源,但在装置拆除过程中未能清理资源,从而导致资源泄漏。
  • The tests themselves cause the resource leakage by allocating resources as part of fixture setup and failing to clean them up during fixture teardown.
可能的解决方案

如果问题出在 SUT 上,那么测试就完成了任务,我们可以修复错误了。如果测试导致了资源泄漏,那么我们必须消除泄漏源。如果泄漏是由于测试失败时未能正确清理而导致的,我们可能需要确保所有测试都执行了保证内联拆卸(请参阅第509页的内联拆卸)或将其转换为使用自动拆卸

If the problem lies in the SUT, then the tests have done their job and we can fix the bug. If the tests are causing the resource leakage, then we must eliminate the source of the leaks. If the leaks are caused by failure to clean up properly when tests fail, we may need to ensure that all tests do Guaranteed In-line Teardown (see In-line Teardown on page 509) or convert them to use Automated Teardown.

一般来说,将所有资源池的大小设置为 1 是一个好主意。这种选择将导致测试更快失败,从而使我们能够更快地确定哪些测试导致了泄漏。

In general, it is a good idea to set the size of all resource pools to 1. This choice will cause the tests to fail much sooner, allowing us to more quickly determine which tests are causing the leak(s).

原因:资源乐观主义

依赖于外部资源的测试具有不确定的结果,具体取决于运行的时间和地点。

A test that depends on external resources has nondeterministic results depending on when or where it is run.

症状

测试在一个环境中运行时通过,而在另一个环境中运行时失败。

A test passes when it is run in one environment and fails when it is run in another environment.

根本原因

在一个环境中可用的资源在另一个环境中不一定可用。

A resource that is available in one environment is not available in another environment.

可能的解决方案

如果可能的话,我们应该通过在测试的夹具设置阶段创建资源来将测试转换为使用Fresh Fixture。这种方法可确保资源在运行的任何地方都存在。它可能需要使用文件的相对寻址来确保无论 SUT 在何处执行,文件系统中的特定位置都存在。

If possible, we should convert the test to use a Fresh Fixture by creating the resource as part of the test's fixture setup phase. This approach ensures that the resource exists wherever it is run. It may necessitate the use of relative addressing of files to ensure that the specific location in the file system exists regardless of where the SUT is executed.

如果必须使用外部资源,则应将资源存储在源代码存储库 [SCM] 中,以便所有测试运行器在相同的环境中运行。

If an external resource must be used, the resources should be stored in the source code repository [SCM] so that all Test Runners run in the same environment.

原因:不可重复的测试

测试首次运行时的行为与后续测试运行时的行为不同。实际上,测试在测试运行过程中与自身进行交互。

A test behaves differently the first time it is run compared with how it behaves on subsequent test runs. In effect, it is interacting with itself across test runs.

症状

测试要么在第一次运行时通过,但在所有后续运行时失败,要么在第一次运行时失败,但在所有后续运行时通过。以下是“通过-失败-失败”的示例:

Either a test passes the first time it is run and fails on all subsequent runs, or it fails the first time and passes on all subsequent runs. Here's an example of what "Pass-Fail-Fail" might look like:

Suite.run()--> 绿色

Suite.run()--> 测试 C 失败

Suite.run()--> 测试 C 失败

用户重置某些内容

Suite.run()--> 绿色

Suite.run()--> 测试 C 失败

Suite.run()-->  Green

Suite.run()-->  Test  C  fails

Suite.run()-->  Test  C  fails

User  resets  something

Suite.run()-->  Green

Suite.run()-->  Test  C  fails

 

以下是“失败-通过-通过”的示例:

Here's an example of what "Fail-Pass-Pass" might look like:

Suite.run()--> 测试 C 失败

Suite.run()--> 绿色

Suite.run()--> 绿色

用户重置某些内容

Suite.run()--> 测试 C 失败

Suite.run()--> 绿色

Suite.run()-->  Test  C  fails

Suite.run()-->  Green

Suite.run()-->  Green

User  resets  something

Suite.run()-->  Test  C  fails

Suite.run()-->  Green

 

请注意,如果我们的测试套件包含几个不可重复的测试,我们可能会看到更像这样的结果:

Be forewarned that if our test suite contains several Unrepeatable Tests, we may see results that look more like this:

Suite.run()--> 测试 C 失败

Suite.run()--> 测试 X 失败

Suite.run()--> 测试 X 失败

用户重置某些内容

Suite.run()--> 测试 C 失败

Suite.run()--> 测试 X 失败

Suite.run()-->  Test  C  fails

Suite.run()-->  Test  X  fails

Suite.run()-->  Test  X  fails

User  resets  something

Suite.run()-->  Test  C  fails

Suite.run()-->  Test  X  fails

 

测试 C 表现出“失败-通过-通过”行为,而测试 X 同时表现出“通过-失败-失败”行为。我们很容易忽略这个问题,因为我们在每种情况下都会看到一个红条;只有我们仔细观察每次运行测试时哪些测试失败,我们才会注意到差异。

Test C exhibits the "Fail-Pass-Pass" behavior, while test X exhibits the "Pass-Fail-Fail" behavior at the same time. It is easy to miss this problem because we see a red bar in each case; we notice the difference only if we look closely to see which tests fail each time we run them.

根本原因

不可重复测试最常见的原因是故意或无意地使用了共享装置。测试可能会修改测试装置,从而导致在测试套件的后续运行期间,装置处于不同的状态。虽然此问题最常发生在预构建装置上(请参阅共享装置),但唯一真正的先决条件是装置比测试运行时间更长。

The most common cause of an Unrepeatable Test is the use—either deliberate or accidental—of a Shared Fixture. A test may be modifying the test fixture such that, during a subsequent run of the test suite, the fixture is in a different state. Although this problem most commonly occurs with a Prebuilt Fixture (see Shared Fixture), the only true prerequisite is that the fixture outlasts the test run.

使用数据库沙箱可以将我们的测试与其他开发人员的测试隔离开来,但它不会阻止我们运行的测试与自身或与我们从同一测试运行器运行的其他测试发生冲突。

The use of a Database Sandbox may isolate our tests from other developers' tests but it won't prevent the tests we run from colliding with themselves or with other tests we run from the same Test Runner.

使用Lazy Setup初始化保存类变量的装置可能会导致测试装置在同一测试套件的后续运行中不会被重新初始化。实际上,我们在从同一测试运行器启动的所有运行之间共享测试装置。

The use of Lazy Setup to initialize a fixture holding class variable can result in the test fixture not being reinitialized on subsequent runs of the same test suite. In effect, we are sharing the test fixture between all runs started from the same Test Runner.

可能的解决方案

因为持久共享夹具是不可重复测试的先决条件,所以我们可以通过对每个测试使用新鲜夹具来消除该问题。为了完全隔离测试,我们必须确保没有任何共享资源(如数据库沙箱)的寿命比各个测试的生命周期长。一种选择是用假数据库(请参阅第551页的假对象)替换数据库。如果必须使用持久数据存储,我们应该对所有数据库键使用不同的生成值(请参阅第723页的生成值),以确保为每个测试和测试运行创建不同的对象。另一种选择是实现自动拆卸,以安全有效地删除所有新创建的对象和行。

Because a persistent Shared Fixture is a prerequisite for an Unrepeatable Test, we can eliminate the problem by using a Fresh Fixture for each test. To fully isolate the tests, we must make sure that no shared resource, such as a Database Sandbox, outlasts the lifetimes of the individual tests. One option is to replace a database with a Fake Database (see Fake Object on page 551). If we must work with a persistent data store, we should use Distinct Generated Values (see Generated Value on page 723) for all database keys to ensure that we create different objects for each test and test run. The other alternative is to implement Automated Teardown to remove all newly created objects and rows safely and efficiently.

原因:试运行之争

当几个人同时进行测试时,测试会随机失败。

Test failures occur at random when several people are running tests simultaneously.

症状

我们正在运行依赖于某些共享外部资源(例如数据库)的测试。从运行测试的单个人员的角度来看,我们可能会看到以下内容:

We are running tests that depend on some shared external resource such as a database. From the perspective of a single person running tests, we might see something like this:

Suite.run() --> 测试 3 失败

Suite.run() --> 测试 2 失败

Suite.run() --> 所有测试通过

Suite.run() --> 测试 1 失败

Suite.run()  -->  Test  3  fails

Suite.run()  -->  Test  2  fails

Suite.run()  -->  All  tests  pass

Suite.run()  -->  Test  1  fails

 

当我们向队友描述我们的问题时,我们发现他们也遇到了同样的问题。当我们其中只有一个人运行测试时,所有测试都会通过。

Upon describing our problem to our teammates, we discover that they are having the same problem at the same time. When only one of us runs tests, all of the tests pass.

影响

测试运行战可能非常令人沮丧,因为越接近代码截止期限,发生测试运行战的概率就越大。这不仅仅是墨菲定律在起作用:在这一点上,测试运行战确实更频繁地发生!随着截止日期的临近,我们倾向于以更频繁的间隔提交较小的更改(想想“最后一刻的错误修复”!)。这反过来又增加了其他人同时运行测试套件的可能性,这本身又增加了同时发生的测试运行之间发生测试冲突的可能性。

A Test Run War can be very frustrating because the probability of it occurring increases the closer we get to a code cutoff deadline. This isn't just Murphy's law kicking in: It really does happen more often at this point! We tend to commit smaller changes at more frequent intervals as the deadline approaches (think "last-minute bug fixing"!). This, in turn, increases the likelihood that someone else will be running the test suite at the same time, which itself increases the likelihood of test collisions between test runs occurring at the same time.

根本原因

只有当我们拥有一个全局共享装置,各个测试可以访问该装置,有时还会对其进行修改时,才会发生测试运行战争。此共享装置可以是必须由测试或 SUT 打开或读取的文件,也可以由测试数据库中的记录组成。

A Test Run War can happen only when we have a globally Shared Fixture that various tests access and sometimes modify. This shared fixture could be a file that must be opened or read by either a test or the SUT, or it could consist of the records in a test database.

数据库争用可能由以下活动引起:

Database contention can be caused by the following activities:

  • 尝试更新或删除一条记录,而另一个测试也在更新同一条记录
  • Trying to update or delete a record while another test is also updating the same record
  • 尝试更新或删除一条记录,而另一个测试对同一条记录具有读锁(悲观锁)
  • Trying to update or delete a record while another test has a read lock (pessimistic locking) on the same record

尝试访问已被另一个测试运行器运行的测试实例打开的文件可能会导致文件争用。

File contention can be caused by an attempt to access a file that has already been opened by another instance of the test running from a different Test Runner.

可能的解决方案

使用Fresh Fixture是解决测试运行战争的首选方案。更简单的解决方案是让每个测试运行者拥有自己的数据库沙箱。这不会测试进行任何更改,但会完全消除测试运行战争的可能性。但是,这不会消除其他不稳定测试的来源,因为测试仍然可以通过共享 Fixture数据库沙箱)进行交互。另一个选择是切换到不可变共享 Fixture,让每个测试在计划更改这些对象时创建新对象。这种方法确实需要更改测试方法

Using a Fresh Fixture is the preferred solution for a Test Run War. An even simpler solution is to give each Test Runner his or her own Database Sandbox. This should not involve making any changes to the tests but will completely eliminate the possibility of a Test Run War. It will not, however, eliminate other sources of Erratic Tests because the tests can still interact through the Shared Fixture (the Database Sandbox). Another option is to switch to an Immutable Shared Fixture by having each test create new objects whenever it plans to change those objects. This approach does require changes to the Test Methods.

如果问题是由一个测试创建的剩余对象或数据库行引起的,这些对象或数据库行会污染后续测试的装置,另一种解决方案是使用自动拆卸来安全高效地清理每个测试。这项措施本身不太可能完全消除测试运行战争,但它可能会降低其发生的频率。

If the problem is caused by leftover objects or database rows created by one test that pollutes the fixture of a later test, another solution is using Automated Teardown to clean up after each test safely and efficiently. This measure, by itself, is unlikely to completely eliminate a Test Run War but it might reduce its frequency.

原因:非确定性测试

测试失败会随机发生,即使只有一个测试运行器正在运行测试。

Test failures occur at random, even when only a single Test Runner is running tests.

症状

我们正在运行测试,每次运行结果都会有所不同,如下所示:

We are running tests and the results vary each time we run them, as shown here:

Suite.run() --> 测试 3 失败

Suite.run() --> 测试 3 崩溃

Suite.run() --> 所有测试通过

Suite.run() --> 测试 3 失败

Suite.run()  -->  Test  3  fails

Suite.run()  -->  Test  3  crashes

Suite.run()  -->  All  tests  pass

Suite.run()  -->  Test  3  fails

 

在与我们的队友交换意见后,我们排除了测试运行战争,因为我们是唯一运行测试的人,或者因为测试设备不在用户或计算机之间共享。

After comparing notes with our teammates, we rule out a Test Run War either because we are the only person running tests or because the test fixture is not shared between users or computers.

与不可重复测试一样,在同一个测试套件中进行多个非确定性测试会使检测故障/错误模式变得更加困难:看起来就像是不同的测试失败了,而不是单个测试产生了不同的结果。

As with an Unrepeatable Test, having multiple Nondeterministic Tests in the same test suite can make it more difficult to detect the failure/error pattern: It looks like different tests are failing rather than a single test producing different results.

影响

调试非确定性测试可能非常耗时且令人沮丧,因为代码每次执行都不同。重现失败可能很困难,而准确描述失败的原因可能需要多次尝试。(一旦确定了原因,通常只需将随机值替换为已知会导致问题的值即可。)

Debugging Nondeterministic Tests can be very time-consuming and frustrating because the code executes differently each time. Reproducing the failure can be problematic, and characterizing exactly what causes the failure may require many attempts. (Once the cause has been characterized, it is often a straightforward process to replace the random value with a value known to cause the problem.)

根本原因

每次运行测试时使用不同的值会导致非确定性测试。当然,有时每次运行同一测试时使用不同的值是个好主意。例如,不同的生成值可以合法地用作数据库中存储的对象的唯一键。但是,将生成的值用作算法的输入,其中 SUT 的行为预计会因不同的值而有所不同,这会导致非确定性测试,如以下示例所示:

Nondeterministic Tests are caused by using different values each time a test is run. Sometimes, of course, it is a good idea to use different values each time the same test is run. For example, Distinct Generated Values may legitimately be used as unique keys for objects stored in a database. Use of generated values as input to an algorithm where the behavior of the SUT is expected to differ for different values can cause Nondeterministic Tests, however, as in the following examples:

  • 整数值,其中负值(甚至零)会被系统以不同方式处理,或者存在最大允许值。如果我们随机生成一个值,测试可能会在某些测试运行中失败,而在其他测试运行中通过。
  • Integer values where negative (or even zero) values are treated differently by the system, or where there is a maximum allowable value. If we generate a value at random, the test could fail in some test runs and pass on others.
  • 字符串值,其中字符串的长度具有允许的最小值或最大值。当我们生成随机或唯一的数值,然后将其转换为字符串表示形式,而没有使用保证长度恒定的显式格式时,通常会意外发生此问题。
  • String values where the length of a string has minimum or maximum allowed values. This problem often occurs accidentally when we generate a random or unique numeric value and then convert it to a string representation without using an explicit format that guarantees the length is constant.

使用随机值似乎是一个好主意,因为它们可以提高我们的测试覆盖率。不幸的是,这种策略降低了我们对测试覆盖率和测试可重复性的理解(这违反了可重复测试原则;参见第26页)。

It might seem like a good idea to use random values because they would improve our test coverage. Unfortunately, this tactic decreases our understanding of the test coverage and the repeatability of our tests (which violates the Repeatable Test principle; see page 26).

另一个可能导致非确定性测试的原因是我们在测试中使用了条件测试逻辑第 200页)。包含它可能会导致在不同的测试运行中执行不同的代码路径,从而导致我们的测试具有不确定性。这样做的一个常见“原因”是灵活测试(请参阅条件测试逻辑)。任何使测试不完全确定的事情都是坏主意!

Another potential cause of Nondeterministic Tests is the use of Conditional Test Logic (page 200) in our tests. Its inclusion can result in different code paths being executed on different test runs, which in turn makes our tests nondeterministic. A common "reason" cited for doing so is the Flexible Test (see Conditional Test Logic). Anything that makes the tests less than completely deterministic is a bad idea!

可能的解决方案

第一步是确保测试以完全线性的方式执行,从而消除所有条件测试逻辑,使测试可重复。然后,我们可以用确定性值替换任何随机值。如果这会导致测试覆盖率较差,我们可以针对未涵盖的有趣案例添加更多测试。确定最佳输入值集的一个好方法是使用等价类边界值。如果使用它们会导致大量测试代码重复,我们可以提取参数化测试第 607页)或将输入值和预期结果放入数据驱动测试第 288页)读取的文件中。

The first step is to make our tests repeatable by ensuring that they execute in a completely linear fashion by removing any Conditional Test Logic. Then we can go about replacing any random values with deterministic values. If this results in poor test coverage, we can add more tests for the interesting cases we aren't covering. A good way to determine the best set of input values is to use the boundary values of the equivalence classes. If their use results in a lot of Test Code Duplication, we can extract a Parameterized Test (page 607) or put the input values and the expected results into a file read by a Data-Driven Test (page 288).

易碎测试

Fragile Test

当 SUT 发生改变但不会影响测试正在执行的部分时,测试将无法编译或运行。

A test fails to compile or run when the SUT is changed in ways that do not affect the part the test is exercising.

症状

Symptoms

我们有一个或多个测试曾经运行并通过,但现在要么无法编译和运行,要么运行时失败。当我们改变了相关 SUT 的行为时,测试结果会发生这样的变化是意料之中的。当我们认为这种变化不应该影响失败的测试,或者我们没有更改任何生产代码或测试时,我们就遇到了脆弱测试的情况。

We have one or more tests that used to run and pass but now either fail to compile and run or fail when they are run. When we have changed the behavior of the SUT in question, such a change in test results is expected. When we don't think the change should have affected the tests that are failing or we haven't changed any production code or tests, we have a case of Fragile Tests.

过去的自动化测试工作经常与自动化测试的“四种敏感性”相冲突。这些敏感性是导致之前通过的完全自动化测试(见第26页)突然开始失败的原因。测试失败的根本原因可以大致分为这四种敏感性之一。虽然每种敏感性可能是由各种特定的测试编码行为引起的,但了解其本身的敏感性是有用的。

Past efforts at automated testing have often run afoul of the "four sensitivities" of automated tests. These sensitivities are what cause Fully Automated Tests (see page 26) that previously passed to suddenly start failing. The root cause for tests failing can be loosely classified into one of these four sensitivities. Although each sensitivity may be caused by a variety of specific test coding behaviors, it is useful to understand the sensitivities in their own right.

影响

Impact

脆弱测试迫使我们在每次修改系统或设备的功能时访问更多测试,从而增加了测试维护成本。当项目依赖于高度增量交付时,它们尤其致命,例如在敏捷开发(例如极限编程)中。

Fragile Tests increase the cost of test maintenance by forcing us to visit many more tests each time we modify the functionality of the system or the fixture. They are particularly deadly when projects rely on highly incremental delivery, as in agile development (such as eXtreme Programming).

故障排除建议

Troubleshooting Advice

我们需要寻找测试失败的模式。我们问自己:“所有失败的测试有什么共同点?”这个问题的答案应该有助于我们了解测试是如何与 SUT 耦合的。然后我们寻找最小化这种耦合的方法。

We need to look for patterns in how the tests fail. We ask ourselves, "What do all of the broken tests have in common?" The answer to this question should help us understand how the tests are coupled to the SUT. Then we look for ways to minimize this coupling.

图 16.2总结了确定我们正在处理哪种敏感度的过程。

Figure 16.2 summarizes the process for determining which sensitivity we are dealing with.

图 16.2. 对易碎测试进行故障排除。

Figure 16.2. Troubleshooting a Fragile Test.

图像

一般的顺序是先问自己测试是否编译失败;如果是,那么接口敏感性可能是罪魁祸首。对于动态语言,我们可能会在运行时看到类型不兼容测试错误——这是接口敏感性的另一个迹象。

The general sequence is to first ask ourselves whether the tests are failing to compile; if so, Interface Sensitivity is likely to blame. With dynamic languages we may see type incompatibility test errors at runtime—another sign of Interface Sensitivity.

如果测试正在运行,但 SUT 提供的结果不正确,我们必须问自己是否更改了代码。如果是这样,我们可以尝试撤消最新的代码更改,看看是否能解决问题。如果该策略可以阻止失败的测试,1那么我们就有了行为敏感性

If the tests are running but the SUT is providing incorrect results, we must ask ourselves whether we have changed the code. If so, we can try backing out of the latest code changes to see if that fixes the problem. If that tactic stops the failing tests,1 then we had Behavior Sensitivity.

如果在撤消最新的代码更改后测试仍然失败,那么一定是发生了其他变化,我们必须处理数据敏感性上下文敏感性。前者仅在我们使用共享夹具第 317页)或修改夹具设置代码时才会发生;否则,我们必须遇到上下文敏感性的情况。

If the tests still fail with the latest code changes backed out, then something else must have changed and we must be dealing with either Data Sensitivity or Context Sensitivity. The former occurs only when we use a Shared Fixture (page 317) or we have modified fixture setup code; otherwise, we must have a case of Context Sensitivity.

虽然这种提问顺序并非万无一失,但十有八九会给出正确答案。买家当心!

While this sequence of asking questions isn't foolproof, it will give the right answer probably nine times out of ten. Caveat emptor!

原因

Causes

脆弱测试可能是由多种不同的根本原因造成的。它们可能是间接测试(请参阅186页的模糊测试),即使用我们修改过的对象来访问其他对象,也可能是急切测试的标志(请参阅224页的断言轮盘),这些测试正在验证太多的功能。脆弱测试也可能是软件过度耦合的症状,这种软件很难在小块中测试(难以测试的代码;请参阅第 209),或者我们缺乏使用测试替身522)对独立部分进行单元测试的经验(过度指定的软件)。

Fragile Tests may be the result of several different root causes. They may be a sign of Indirect Testing (see Obscure Test on page 186)—that is, using the objects we modified to access other objects—or they may be a sign that we have Eager Tests (see Assertion Roulette on page 224) that are verifying too much functionality. Fragile Tests may also be symptoms of overcoupled software that is hard to test in small pieces (Hard-to-Test Code; see page 209) or our lack of experience with unit testing using Test Doubles (page 522) to test pieces in isolation (Overspecified Software).

无论其根本原因是什么,脆弱测试通常都是四种敏感性之一。让我们首先更详细地了解一下它们;然后我们将研究一些更详细的例子,了解具体原因如何改变测试输出。

Regardless of their root cause, Fragile Tests usually show up as one of the four sensitivities. Let's start by looking at them in a bit more detail; we'll then examine some more detailed examples of how specific causes change test output.

原因:界面敏感性

当测试使用的 SUT 接口的某些部分发生变化,导致测试无法编译或运行时,就会发生接口敏感性。

Interface Sensitivity occurs when a test fails to compile or run because some part of the interface of the SUT that the test uses has changed.

症状

在静态类型语言中,接口敏感性通常表现为编译失败。在动态类型语言中,它仅在我们运行测试时出现。用动态类型语言编写的测试在调用已修改的应用程序编程接口 (API)(通过方法名称更改或方法签名更改)时可能会遇到测试错误。或者,测试可能无法找到通过用户界面与 SUT 交互所需的用户界面元素。通过用户界面2与 SUT 交互的记录测试第 278页)特别容易出现此问题。

In statically typed languages, Interface Sensitivity usually shows up as a failure to compile. In dynamically typed languages, it shows up only when we run the tests. A test written in a dynamically typed language may experience a test error when it invokes an application programming interface (API) that has been modified (via a method name change or method signature change). Alternatively, the test may fail to find a user interface element it needs to interact with the SUT via a user interface. Recorded Tests (page 278) that interact with the SUT through a user interface2 are particularly prone to this problem.

可能的解决方案

失败的原因通常相当明显。测试失败的点(编译或执行)通常会指出问题的位置。测试很少会在变化点之后继续运行 — 毕竟,变化本身才是导致测试错误的原因。

The cause of the failures is usually reasonably apparent. The point at which the test fails (to compile or execute) will usually point out the location of the problem. It is rare for the test to continue to run beyond the point of change—after all, it is the change itself that causes the test error.

当接口仅在内部(组织或应用程序内)和通过自动化测试使用时,SUT API 封装(请参见第599页的测试实用程序方法)是接口敏感性的最佳解决方案它降低了 API 更改的成本和影响,因此不会阻止进行必要的更改。实现SUT API 封装的一种常用方法是通过定义用于表达测试的高级语言(请参见第41页)。测试语言中的动词由封装层转换为适当的方法调用,这样,当接口以某种向后兼容的方式更改时,封装层就是唯一需要修改的软件。“测试语言”可以采用测试实用程序方法的形式实现,例如创建方法(第 415页)和验证方法(请参见第474页的自定义断言),它们将 SUT 的 API 隐藏在测试中。

When the interface is used only internally (within the organization or application) and by automated tests, SUT API Encapsulation (see Test Utility Method on page 599) is the best solution for Interface Sensitivity. It reduces the cost and impact of changes to the API and, therefore, does not discourage necessary changes from being made. A common way to implement SUT API Encapsulation is through the definition of a Higher-Level Language (see page 41) that is used to express the tests. The verbs in the test language are translated into the appropriate method calls by the encapsulation layer, which is then the only software that needs to be modified when the interface is altered in somewhat backward-compatible ways. The "test language" can be implemented in the form of Test Utility Methods such as Creation Methods (page 415) and Verification Methods (see Custom Assertion on page 474) that hide the API of the SUT from the test.

避免接口敏感性的唯一其他方法是对接口进行严格的变更控制。当接口的客户端是外部的和匿名的(例如 Windows DLL 的客户端)时,此策略可能是唯一可行的选择。在这些情况下,通常需要遵循协议来对接口进行更改。也就是说,所有更改都必须向后兼容;在删除旧版本的方法之前,必须弃用它们,并且弃用的方法必须存在最少数量的版本或经过的时间。

The only other way to avoid Interface Sensitivity is to put the interface under strict change control. When the clients of the interface are external and anonymous (such as the clients of Windows DLLs), this tactic may be the only viable alternative. In these cases, a protocol usually applies to making changes to interfaces. That is, all changes must be backward compatible; before older versions of methods can be removed, they must be deprecated, and deprecated methods must exist for a minimum number of releases or elapsed time.

原因:行为敏感性

当 SUT 的变化导致其他测试失败时,就会发生行为敏感性。

Behavior Sensitivity occurs when changes to the SUT cause other tests to fail.

症状

当向 SUT 添加新功能或修复错误时,曾经通过的测试突然开始失败。

A test that once passed suddenly starts failing when a new feature is added to the SUT or a bug is fixed.

根本原因

测试可能会失败,因为它们正在验证的功能已被修改。这种结果并不一定表示存在行为敏感性,因为这是进行回归测试的全部原因。在以下任何情况下,都属于行为敏感性的情况:

Tests may fail because the functionality they are verifying has been modified. This outcome does not necessarily signal a case of Behavior Sensitivity because it is the whole reason for having regression tests. It is a case of Behavior Sensitivity in any of the following circumstances:

  • 回归测试用来设置 SUT 预测试状态的功能已被修改。
  • The functionality the regression tests use to set up the pre-test state of the SUT has been modified.
  • 回归测试用来验证 SUT 测试后状态的功能已被修改。
  • The functionality the regression tests use to verify the post-test state of the SUT has been modified.
  • 回归测试用来拆除装置的代码已经改变。
  • The code the regression tests use to tear down the fixture has been changed.

如果更改的代码不是我们正在验证的 SUT 的一部分,那么我们正在处理上下文敏感性。也就是说,我们可能测试了太大的 SUT。在这种情况下,我们真正需要做的是将 SUT 分为我们正在验证的部分和该部分所依赖的组件。

If the code that changed is not part of the SUT we are verifying, then we are dealing with Context Sensitivity. That is, we may be testing too large a SUT. In such a case, what we really need to do is to separate the SUT into the part we are verifying and the components on which that part depends.

可能的解决方案

在夹具设置期间使用的有关 SUT 行为的任何新的错误假设都可以封装在创建方法中。同样,有关 SUT 测试后状态细节的假设可以封装在自定义断言验证方法中。虽然这些措施不会消除假设发生变化时更新测试代码的需要,但它们确实减少了需要更改的测试代码量。

Any newly incorrect assumptions about the behavior of the SUT used during fixture setup may be encapsulated behind Creation Methods. Similarly, assumptions about the details of post-test state of the SUT can be encapsulated in Custom Assertions or Verification Methods. While these measures won't eliminate the need to update test code when the assumptions change, they certainly do reduce the amount of test code that needs to be changed.

原因:数据敏感性

数据敏感性是指测试失败,因为用于测试 SUT 的数据已被修改。这种敏感性最常发生在测试数据库的内容发生变化时。

Data Sensitivity occurs when a test fails because the data being used to test the SUT has been modified. This sensitivity most commonly arises when the contents of the test database change.

症状

在下列任一情况下,曾经通过的测试突然开始失败:

A test that once passed suddenly starts failing in any of the following circumstances:

  • 数据被添加到保存 SUT 测试前状态的数据库中。
  • Data is added to the database that holds the pre-test state of the SUT.
  • 数据库中的记录被修改或删除。
  • Records in the database are modified or deleted.
  • 修改了设置标准夹具(第 305页) 的代码。
  • The code that sets up a Standard Fixture (page 305) is modified.
  • 共享装置在第一次使用其的测试之前被修改。
  • A Shared Fixture is modified before the first test that uses it.

在所有这些情况下,我们都必须使用标准 Fixture,它可以是新 Fixture第 311页),也可以是共享 Fixture,例如预建 Fixture(请参阅共享 Fixture)。

In all of these cases, we must be using a Standard Fixture, which may be either a Fresh Fixture (page 311) or a Shared Fixture such as a Prebuilt Fixture (see Shared Fixture).

根本原因

测试可能会失败,因为测试中的结果验证逻辑会查找数据库中不再存在的数据,或者使用意外包含新添加记录的搜索条件。另一个可能导致失败的原因是,SUT 正在使用引用丢失或修改的数据的输入进行测试,因此 SUT 的行为会有所不同。

Tests may fail because the result verification logic in the test looks for data that no longer exists in the database or uses search criteria that accidentally include newly added records. Another potential cause of failure is that the SUT is being exercised with inputs that reference missing or modified data and, therefore, the SUT behaves differently.

在所有情况下,测试都会对数据库中存在哪些数据做出假设——而这些假设却被违反了。

In all cases, the tests make assumptions about which data exist in the database—and those assumptions are violated.

可能的解决方案

如果在测试的 SUT 阶段发生故障,我们需要查看正在执行的逻辑的先决条件,并确保它们没有受到数据库最近更改的影响。

In those cases where the failures occur during the exercise SUT phase of the test, we need to look at the pre-conditions of the logic we are exercising and make sure they have not been affected by recent changes to the database.

大多数情况下,失败发生在结果验证过程中。我们需要检查结果验证逻辑,以确保它没有对哪些数据存在做出任何不合理的假设。如果有,我们可以修改验证逻辑。

In most cases, the failures occur during result verification. We need to examine the result verification logic to ensure that it does not make any unreasonable assumptions about which data exists. If it does, we can modify the verification logic.


为什么我们需要 100 个客户?

我的一位软件开发同事正在从事一个项目,担任分析师。有一天,她的经理走进她的办公室,问道:“为什么你要求在测试数据库实例中创建 100 个唯一客户?”

作为一名系统分析师,我的同事负责帮助业务分析师定义一个大型复杂项目的需求和验收测试。她想实现测试自动化,但必须克服几个障碍。最大的障碍之一是 SUT 的大部分数据来自上游系统 — 尝试手动生成这些数据太复杂了。

系统分析员想出了一种方法,从电子表格中捕获的测试生成 XML。对于测试的夹具设置部分,她将 XML 转换为 QaRun(一个记录和回放测试工具-请参阅第278页的“记录的测试”)脚本,这些脚本会通过用户界面将数据加载到上游系统中。由于运行这些脚本需要一段时间,并且数据需要一段时间才能传输到 SUT,所以系统分析员必须提前运行这些脚本。这意味着新鲜夹具(第 311页)策略是无法实现的;预建夹具第 429页)是她能做的最好的事情。为了避免共享夹具(第317页)必定会导致的交互测试(请参阅第228页的“不稳定测试” ),系统分析员决定使用基于每个测试的唯一客户编号的数据库分区方案来实现虚拟数据库沙箱(第650页) 。这样,一个测试的任何副作用就不会影响任何其他测试。

考虑到她有大约 100 个测试需要自动化,系统分析师需要在数据库中定义大约 100 个测试客户。这就是她告诉经理的。



Why Do We Need 100 Customers?

A software development coworker of mine was working on a project as an analyst. One day, the manager she was working for came into her office and asked, "Why have you requested 100 unique customers be created in the test database instance?"

As a systems analyst, my coworker was responsible for helping the business analysts define the requirements and the acceptance tests for a large, complex project. She wanted to automate the tests but had to overcome several hurdles. One of the biggest hurdles was the fact that the SUT got much of its data from an upstream system—it was too complex to try to generate this data manually.

The systems analyst came up with a way to generate XML from tests captured in spreadsheets. For the fixture setup part of the tests, she transformed the XML into QaRun (a Record and Playback Test tool—see Recorded Test on page 278) scripts that would load the data into the upstream system via the user interface. Because it took a while to run these scripts and for the data to make its way downstream to the SUT, the systems analyst had to run these scripts ahead of time. This meant that a Fresh Fixture (page 311) strategy was unachievable; a Prebuilt Fixture (page 429) was the best she could do. In an attempt to avoid the Interacting Tests (see Erratic Test on page 228) that were sure to result from a Shared Fixture (page 317), the systems analyst decided to implement a virtual Database Sandbox (page 650) using a Database Partitioning Scheme based on a unique customer number for each test. This way, any side effects of one test couldn't affect any other tests.

Given that she had about 100 tests to automate, the systems analyst needed about 100 test customers defined in the database. And that's what she told her manager.


 

即使问题在于 SUT 的输入引用了不存在或修改过的数据,失败也可能出现在结果验证逻辑中。这可能需要检查 SUT 的“之后”状态(与预期的测试后状态不同)并追溯它以发现它与我们的预期不符的原因。这应该会揭示 SUT 输入与测试开始执行之前存在的数据之间的不匹配。

The failure can show up in the result verification logic even if the problem is that the inputs of the SUT refer to nonexistent or modified data. This may require examining the "after" state of the SUT (which differs from the expected post-test state) and tracing it back to discover why it does not match our expectations. This should expose the mismatch between SUT inputs and the data that existed before the test started executing.

数据敏感性的最佳解决方案是使测试独立于数据库的现有内容,即使用Fresh Fixture。如果这不可能,我们可以尝试使用某种数据库分区方案(请参阅第650页的数据库沙箱)来确保为一个测试修改的数据不会与其他测试使用的数据重叠。(有关示例,请参阅第244页的侧栏“为什么我们需要 100 个客户? ” )

The best solution to Data Sensitivity is to make the tests independent of the existing contents of the database—that is, to use a Fresh Fixture. If this is not possible, we can try using some sort of Database Partitioning Scheme (see Database Sandbox on page 650) to ensure that the data modified for one test does not overlap with the data used by other tests. (See the sidebar "Why Do We Need 100 Customers?" on page 244 for an example.)

另一个解决方案是验证对数据所做的更改是否正确。Delta断言第 485页)比较数据的前后“快照”,从而忽略未更改的数据。它们消除了将有关整个装置的知识硬编码到测试结果验证阶段的需要。

Another solution is to verify that the right changes have been made to the data. Delta Assertions (page 485) compare before and after "snapshots" of the data, thereby ignoring data that hasn't changed. They eliminate the need to hard-code knowledge about the entire fixture into the result verification phase of the test.

原因:上下文敏感性

当测试因 SUT 执行上下文的状态或行为发生了某种变化而失败时,就会发生上下文敏感性。

Context Sensitivity occurs when a test fails because the state or behavior of the context in which the SUT executes has changed in some way.

症状

曾经通过的测试突然因神秘原因失败。与不稳定测试(第 228页) 不同,该测试在短时间内重复运行时会产生一致的结果。不同之处在于,无论如何运行,它都会始终失败。

A test that once passed suddenly starts failing for mysterious reasons. Unlike with an Erratic Test (page 228), the test produces consistent results when run repeatedly over a short period of time. What is different is that it consistently fails regardless of how it is run.

根本原因

测试可能由于两个原因而失败:

Tests may fail for two reasons:

  • 他们正在验证的功能在某种程度上取决于时间或日期。
  • The functionality they are verifying depends in some way on the time or date.
  • SUT 所依赖的某些其他代码或系统的行为已经发生改变。
  • The behavior of some other code or system(s) on which the SUT depends has changed.

上下文敏感性的一个主要来源是我们对要验证的 SUT 感到困惑。回想一下,SUT 是我们打算验证的软件的任何部分。在单元测试时,它应该是整个系统或应用程序的很小一部分。未能隔离特定单元(例如,类或方法)必然会导致上下文敏感性,因为我们最终会同时测试太多软件。应该由测试控制的间接输入因此被留给了机会。如果有人修改了依赖的组件(DOC),我们的测试就会失败。

A major source of Context Sensitivity is confusion about which SUT we are intending to verify. Recall that the SUT is whatever piece of software we are intending to verify. When unit testing, it should be a very small part of the overall system or application. Failure to isolate the specific unit (e.g., class or method) is bound to lead to Context Sensitivity because we end up testing too much software all at once. Indirect inputs that should be controlled by the test are then left to chance. If someone then modifies a depended-on component (DOC), our tests fail.

为了消除上下文敏感性,我们必须追踪 SUT 的哪些间接输入发生了变化以及变化的原因。如果系统包含任何与日期或时间相关的逻辑,我们应该检查该逻辑,看看月份长度或其他类似因素是否是导致问题的原因。

To eliminate Context Sensitivity, we must track down which indirect input to the SUT has changed and why. If the system contains any date- or time-related logic, we should examine this logic to see whether the length of the month or other similar factors could be the cause of the problem.

如果 SUT 依赖于任何其他系统的输入,我们应该检查这些输入,看看最近是否有任何变化。与这些其他系统之前交互的日志对于与故障场景的日志进行比较非常有用。

If the SUT depends on input from any other systems, we should examine these inputs to see if anything has changed recently. Logs of previous interactions with these other systems are very useful for comparison with logs of the failure scenarios.

如果问题时有时无,我们应该寻找与问题何时解决和何时失败相关的模式。请参阅不稳定测试,了解有关上下文敏感性可能原因的更详细讨论。

If the problem comes and goes, we should look for patterns related to when it passes and when it fails. See Erratic Test for a more detailed discussion of possible causes of Context Sensitivity.

可能的解决方案

如果我们的测试要具有确定性,我们需要控制 SUT 的所有输入。如果我们依赖于其他系统的输入,我们可能需要使用由测试配置和安装的测试桩(第 529页) 来控制这些输入。如果系统包含任何特定于时间或日期的逻辑,我们需要能够在测试中控制系统时钟。这可能需要使用虚拟时钟 [VCTP]来截断系统时钟,这样测试就可以设置开始时间或日期,并可能模拟时间的流逝。

We need to control all the inputs of the SUT if our tests are to be deterministic. If we depend on inputs from other systems, we may need to control these inputs by using a Test Stub (page 529) that is configured and installed by the test. If the system contains any time- or date-specific logic, we need to be able to control the system clock as part of our testing. This may necessitate stubbing out the system clock with a Virtual Clock [VCTP] that gives the test a way to set the starting time or date and possibly to simulate the passage of time.

原因:软件规格过高

也称为

Also known as

过度耦合测试

Overcoupled Test

测试过多地说明了软件应如何构造或运行。这种形式的行为敏感性(请参阅第 239页的脆弱性测试)与称为行为验证(第468页)的测试样式相关。它的特点是大量使用模拟对象(第544页)来构建跨层测试。主要问题是测试描述的是软件应该如何做某事,而不是应该实现什么。也就是说,只有以特定方式实现软件,测试才会通过。可以通过尽可能应用先使用前门(请参阅第40页)的原则来避免此问题,以避免在测试中编码过多有关 SUT 实现的知识。

A test says too much about how the software should be structured or behave. This form of Behavior Sensitivity (see Fragile Test on page 239) is associated with the style of testing called Behavior Verification (page 468). It is characterized by extensive use of Mock Objects (page 544) to build layer-crossing tests. The main issue is that the tests describe how the software should do something, not what it should achieve. That is, the tests will pass only if the software is implemented in a particular way. This problem can be avoided by applying the principle Use the Front Door First (see page 40) whenever possible to avoid encoding too much knowledge about the implementation of the SUT into the tests.

原因:敏感平等

要验证的对象被转换为字符串,并与预期字符串进行比较。这是行为敏感性的一个例子,因为测试对不属于验证范围的行为很敏感。我们也可以将其视为接口敏感性的一个例子,其中接口的语义已经改变。无论哪种方式,问题都源于测试的编码方式;使用对象的字符串表示来根据预期值验证它们只会自找麻烦。

Objects to be verified are converted to strings and compared with an expected string. This is an example of Behavior Sensitivity in that the test is sensitive to behavior that it is not in the business of verifying. We could also think of it as a case of Interface Sensitivity where the semantics of the interface have changed. Either way, the problem arises from the way the test was coded; using the string representations of objects for verifying them against expected values is just asking for trouble.

原因:固定装置易碎

标准装置被修改以适应新测试时,其他几个测试会失败。这是数据敏感性上下文敏感性的别名,具体取决于相关装置的性质。

When a Standard Fixture is modified to accommodate a new test, several other tests fail. This is an alias for either Data Sensitivity or Context Sensitivity depending on the nature of the fixture in question.

进一步阅读

敏感平等脆弱性固定装置最早在 [RTC] 中描述这是第一篇关于测试异味和重构测试代码的论文。四种敏感性最早在[ARTRP] 中描述,其中还描述了几种在记录测试中避免脆弱测试的方法。

Sensitive Equality and Fragile Fixture were first described in [RTC], which was the first paper published on test smells and refactoring test code. The four sensitivities were first described in [ARTRP], which also described several ways to avoid Fragile Tests in Recorded Tests.

频繁调试

Frequent Debugging

需要手动调试才能确定大多数测试失败的原因。

Manual debugging is required to determine the cause of most test failures.

症状

Symptoms

也称为

Also known as

手动调试

Manual Debugging

测试运行导致测试失败或测试错误。测试运行器第 377页)的输出不足以让我们确定问题所在。因此,我们必须使用交互式调试器(或在整个代码中散布打印语句)来确定哪里出了问题。

A test run results in a test failure or a test error. The output of the Test Runner (page 377) is insufficient for us to determine the problem. Thus we have to use an interactive debugger (or sprinkle print statements throughout the code) to determine where things are going wrong.

如果这种情况是例外,我们不必担心。但是,如果大多数测试失败都需要这种调试,那么我们就遇到了频繁调试的情况。

If this case is an exception, we needn't worry about it. If most test failures require this kind of debugging, however, we have a case of Frequent Debugging.

原因

Causes

频繁调试是由于我们的自动化测试套件缺乏缺陷定位(见第22370页的断言消息)或通过测试失败的模式告诉我们出了什么问题。如果没有:

Frequent Debugging is caused by a lack of Defect Localization (see page 22) in our suite of automated tests. The failed tests should tell us what went wrong either through their individual failure messages (see Assertion Message on page 370) or through the pattern of test failures. If they don't:

  • 我们可能缺少能够指出单个类内部逻辑错误的详细单元测试。
  • We may be missing the detailed unit tests that would point out a logic error inside an individual class.
  • 我们可能遗漏了类集群(即组件)的组件测试,而这些测试会指出各个类之间的集成错误。当我们大量使用Mock 对象第 544页)来替换依赖对象,但依赖对象的单元测试与Mock 对象的编程行为方式不匹配时,就会发生这种情况。
  • We may be missing the component tests for a cluster of classes (i.e., a component) that would point out an integration error between the individual classes. This can happen when we use Mock Objects (page 544) extensively to replace depended-on objects but the unit tests of the depended-on objects don't match the way the Mock Objects are programmed to behave.

当我编写了高级(功能或组件)测试但未能为各个方法编写所有单元测试时,我最常遇到此问题。(有些人将这种方法称为故事测试驱动开发,以将其与单元测试驱动开发区分开来,在单元测试驱动开发中,每一小段代码都是由失败的单元测试产生的。)

I've encountered this problem most frequently when I wrote higher-level (functional or component) tests but failed to write all the unit tests for the individual methods. (Some people would call this approach storytest-driven development to distinguish it from unit test-driven development, in which every little bit of code is pulled into existence by a failing unit test.)

频繁调试也可能是由不经常运行的测试(请参阅第 268页的生产错误)。如果我们在对软件进行每次小改动后都运行测试,我们就能轻松记住自上次运行测试以来我们做了哪些更改。因此,当测试失败时,我们不必花费大量时间对软件进行故障排除以发现错误所在 - 我们知道它在哪里,因为我们记得它放在那里!

Frequent Debugging can also be caused by Infrequently Run Tests (see Production Bugs on page 268). If we run our tests after every little change we make to the software, we can easily remember what we changed since the last time we ran the tests. Thus, when a test fails, we don't have to spend a lot of time troubleshooting the software to discover where the bug is—we know where it is because we remember putting it there!

影响

Impact

手动调试是一个缓慢而繁琐的过程。很容易忽略错误的细微迹象,并花费大量时间追踪单个逻辑错误。频繁调试会降低生产率,并使开发计划变得难以预测,因为单个手动调试会话可能会将开发软件所需的时间延长半天或更长时间。

Manual debugging is a slow, tedious process. It is easy to overlook subtle indications of a bug and spend many hours tracking down a single logic error. Frequent Debugging reduces productivity and makes development schedules much less predictable because a single manual debugging session could extend the time required to develop the software by half a day or more.

解决方案模式

Solution Patterns

如果我们缺少某个功能的客户测试,而手动用户测试发现了任何自动化测试都未发现的问题,那么我们可能遇到了未经测试的需求(请参阅生产错误)。我们可以问自己,“什么样的自动化测试可以避免手动调试会话?”更好的是,一旦我们确定了问题,我们就可以编写一个测试来暴露它。然后我们可以使用失败的测试进行测试驱动的错误修复。如果我们怀疑这是一个普遍存在的问题,我们可以创建一个开发任务来识别和编写任何需要填补我们刚刚发现的空白的额外测试。

If we are missing the customer tests for a piece of functionality and manual user testing has revealed a problem not exposed by any automated tests, we probably have a case of Untested Requirements (see Production Bugs). We can ask ourselves, "What kind of automated test would have prevented the manual debugging session?" Better yet, once we have identified the problem, we can write a test that exposes it. Then we can use the failing test to do test-driven bug fixing. If we suspect this to be a widespread problem, we can create a development task to identify and write any additional tests that would be required to fill the gap we just exposed.

进行真正的测试驱动开发是避免导致频繁调试的最佳方法。我们应该尽可能接近应用程序的外表,进行故事测试驱动开发——也就是说,我们应该为各个类编写单元测试,并为相关类的集合编写组件测试,以确保我们有良好的缺陷定位

Doing true test-driven development is the best way to avoid the circumstances that lead to Frequent Debugging. We should start as close as possible to the skin of the application and do storytest-driven development—that is, we should write unit tests for individual classes as well as component tests for the collections of related classes to ensure we have good Defect Localization.

人工干预

Manual Intervention

每次运行测试时都需要人员执行一些手动操作。

A test requires a person to perform some manual action each time it is run.

症状

Symptoms

运行测试的人员必须在测试运行前或测试运行过程中手动执行某些操作;否则,测试将失败。测试运行者可能需要手动验证测试结果。

The person running the test must do something manually either before the test is run or partway through the test run; otherwise, the test fails. The Test Runner may need to verify the results of the test manually.

影响

Impact

自动化测试的目的在于尽早获得软件中出现问题的反馈。如果获取反馈的成本太高(即以人工干预的形式),我们可能不会经常运行测试,也不会经常获得反馈。如果我们不经常获得反馈,我们可能会在测试运行之间引入大量问题,最终导致频繁调试第 248页)和高测试维护成本(第265页)。

Automated tests are all about getting early feedback on problems introduced into the software. If the cost of getting that feedback is too high—that is, if it takes the form of Manual Intervention—we likely won't run the tests very often and we won't get the feedback very often. If we don't get that feedback very often, we'll probably introduce lots of problems between test runs, which will ultimately lead to Frequent Debugging (page 248) and High Test Maintenance Cost (page 265).

人工干预也使得完全自动化的集成构建 [SCM] 和回归测试过程变得不切实际。

Manual Intervention also makes it impractical to have a fully automated Integration Build [SCM] and regression test process.

原因

Causes

需要人工干预的原因多种多样,就像我们的软件执行或遇到的事情一样。以下是需要人工干预的一些一般类别。不过,这个列表绝不是详尽无遗的。

The causes of Manual Intervention are as varied as the kinds of things our software does or encounters. The following are some general categories of the kinds of issues that require Manual Intervention. This list is by no means exhaustive, though.

原因:手动夹具设置
症状

在运行自动化测试之前,必须手动设置测试环境。此活动可能采取配置服务器、启动服务器进程或运行脚本来设置预构建装置(第429页) 的形式。

A person has to set up the test environment manually before the automated tests can be run. This activity may take the form of configuring servers, starting server processes, or running scripts to set up a Prebuilt Fixture (page 429).

根本原因

这个问题通常是由于没有注意测试的装置设置阶段的自动化而导致的。也可能是由于 SUT 中的组件之间耦合过度,导致我们无法在开发环境中测试系统中的大多数代码。

This problem is typically caused by a lack of attention to automating the fixture setup phase of the test. It may also be caused by excessive coupling between components in the SUT that prevents us from testing a majority of the code in the system inside the development environment.

可能的解决方案

我们需要确保编写的是完全自动化的测试。这可能需要开放测试专用 API,以允许测试设置装置。如果问题与无法在开发环境中运行软件有关,我们可能需要重构软件,将 SUT 与原本需要手动完成的步骤分离开来。

We need to make sure that we are writing Fully Automated Tests. This may require opening up test-specific APIs to allow tests to set up the fixture. Where the issue is related to an inability to run the software in the development environment, we may need to refactor the software to decouple the SUT from the steps that would otherwise need to be done manually.

原因:手动结果验证
症状

我们可以运行测试,但它们几乎总是通过——即使我们知道 SUT 没有返回正确的结果。

We can run the tests but they almost always pass—even when we know that the SUT is not returning the correct results.

根本原因

如果我们编写的测试不是自检测试(参见第 26页),我们就会有一种虚假的安全感,因为只有抛出错误/异常时测试才会失败。

If the tests we write are not Self-Checking Tests (see page 26), we can be given a false sense of security because tests will fail only if an error/exception is thrown.

可能的解决方案

我们可以确保我们的测试都是自检的,方法是在测试方法(第 348页) 中包含结果验证逻辑(例如对断言方法(第362页) 的调用) 。

We can ensure that our tests are all self-checking by including result verification logic such as calls to Assertion Methods (page 362) within the Test Methods (page 348).

原因:手动事件注入
症状

在测试执行期间,必须有人进行干预以执行一些手动操作,测试才能继续进行。

A person must intervene during test execution to perform some manual action before the test can proceed.

根本原因

SUT 中的许多事件很难在程序控制下生成。例如拔掉网线、断开数据库连接以及单击用户界面上的按钮。

Many events in a SUT are hard to generate under program control. Examples include unplugging network cables, bringing down database connections, and clicking buttons on a user interface.

影响

如果一个人需要手动做某件事,这不仅增加了运行测试的工作量,而且确保测试不能在无人看管的情况下运行。这会破坏任何尝试完全自动化构建和测试周期的尝试。

If a person needs to do something manually, it both increases the effort to run the test and ensures that the test cannot be run unattended. This torpedoes any attempt to do a fully automated build-and-test cycle.

可能的解决方案

最好的解决方案是找到不需要真人执行手动操作的软件测试方法。如果事件通过异步事件报告给 SUT,我们可以让测试方法直接调用 SUT,并向其传递模拟事件对象。如果 SUT 遇到的情况是来自系统其他部分的同步响应,我们可以通过将 SUT 的某些部分替换为测试桩(第529页)来控制间接输入,该桩模拟我们想要向 SUT 展示的情况。

The best solution is to find ways to test the software that do not require a real person to do the manual actions. If the events are reported to the SUT through asynchronous events, we can have the Test Method invoke the SUT directly, passing it a simulated event object. If the SUT experiences the situation as a synchronous response from some other part of the system, we can get control of the indirect inputs by replacing some part of the SUT with a Test Stub (page 529) that simulates the circumstances to which we want to expose the SUT.

进一步阅读

有关如何控制 SUT 的间接输入的更详细描述,请参阅第 11 章使用测试替身” 。

Refer to Chapter 11, Using Test Doubles, for a much more detailed description of how to get control of the indirect inputs of the SUT.

缓慢的测试

Slow Tests

测试运行时间太长。

The tests take too long to run.

症状

Symptoms

测试需要很长时间才能运行,因此开发人员不会在每次对 SUT 进行更改时都运行它们。相反,开发人员会等到下一次咖啡休息或其他中断后再运行它们。或者,每当他们运行测试时,他们都会四处走动并与其他团队成员聊天(或玩 Doom 或上网或……)。

The tests take long enough to run that developers don't run them every time they make a change to the SUT. Instead, the developers wait until the next coffee break or another interruption before running them. Or, whenever they run the tests, they walk around and chat with other team members (or play Doom or surf the Internet or . . .).

影响

Impact

缓慢的测试显然会带来直接的代价:它们会降低测试人员的工作效率。当我们测试代码时,每次运行测试都会浪费宝贵的几秒钟;当我们提交更改之前需要运行所有测试时,我们将面临更长的等待时间。

Slow Tests obviously have a direct cost: They reduce the productivity of the person running the test. When we are test driving the code, we'll waste precious seconds every time we run our tests; when it is time to run all the tests before we commit our changes, we'll have an even more significant wait time.

缓慢的测试也会带来很多间接成本:

Slow Tests also have many indirect costs:

  • 由于我们需要等待合并所有更改后测试运行,因此更长时间持有“集成令牌”会造成瓶颈。
  • The bottleneck created by holding the "integration token" longer because we need to wait for the tests to run after merging all our changes.
  • 等待测试运行完成的人分散其他人注意力的时间。
  • The time during which other people are distracted by the person waiting for his or her test run to finish.
  • 在调试器中查找上次运行测试后某个时间插入的问题所花费的时间。测试运行的时间越长,我们就越不可能确切地记得我们做了什么来破坏测试。这种成本是由于自动化单元测试提供的快速反馈的崩溃造成的。
  • The time spent in debuggers finding a problem that was inserted sometime after the last time we ran the test. The longer it has been since the test was run, the less likely we are to remember exactly what we did to break the test. This cost is a result of the breakdown of the rapid feedback that automated unit tests provide.

对慢速测试的常见反应是立即使用共享装置(第317页)。不幸的是,这种方法几乎总是会导致其他问题,包括不稳定测试(第228页)。更好的解决方案是使用伪对象(第 551页) 将慢速组件(如数据库)替换为更快的组件。但是,如果所有其他方法都失败了,我们必须使用某种共享装置,我们应该尽可能使其不可变。

A common reaction to Slow Tests is to immediately go for a Shared Fixture (page 317). Unfortunately, this approach almost always results in other problems, including Erratic Tests (page 228). A better solution is to use a Fake Object (page 551) to replace slow components (such as the database) with faster ones. However, if all else fails and we must use some kind of Shared Fixture, we should make it immutable if at all possible.

故障排除建议

Troubleshooting Advice

测试速度慢可能是由 SUT 的构建和测试方式或测试设计方式导致的。有时问题很明显 — 我们可以在运行测试时看到绿色条增长。执行过程中可能会出现明显的停顿;我们可能会看到测试方法(第 348) 中编码的明确延迟。但是,如果原因不明显,我们可以运行不同的测试子集(或子套件)来查看哪些测试运行得快,哪些测试运行时间长。

Slow Tests can be caused either by the way the SUT is built and tested or by the way the tests are designed. Sometimes the problem is obvious—we can just watch the green bar grow as we run the tests. There may be notable pauses in the execution; we may see explicit delays coded in a Test Method (page 348). If the cause is not obvious, however, we can run different subsets (or subsuites) of tests to see which ones run quickly and which ones take a long time to run.

可以使用性能分析工具来查看测试执行中哪些地方花费了额外的时间。当然,xUnit 为我们提供了一种简单的方法来构建我们自己的迷你性能分析器:我们可以编辑测试用例超类(第 638页)的setUp和方法。然后,我们将开始/结束时间或测试持续时间写入日志文件,以及测试用例类(第 373页) 和测试方法的名称。最后,我们将此文件导入电子表格,按持续时间排序,瞧 — 我们找到了罪魁祸首。执行时间最长的测试最值得我们集中精力。tearDown

A profiling tool can come in handy to see where we are spending the extra time in test execution. Of course, xUnit gives us a simple means to build our own mini-profiler: We can edit the setUp and tearDown methods of our Testcase Superclass (page 638). We then write out the start/end times or test duration into a log file, along with the name of the Testcase Class (page 373) and Test Method. Finally, we import this file into a spreadsheet, sort by duration, and voila—we have found the culprits. The tests with the longest execution times are the ones on which it will be most worthwhile to focus our efforts.

原因

Causes

测试速度慢的具体原因可能在于我们构建 SUT 的方式,也可能在于我们编写测试的方式。有时,SUT 的构建方式迫使我们以一种导致测试速度变慢的方式编写测试。对于遗留代码或以“最后测试”观点构建的代码,这尤其成问题。

The specific cause of the Slow Tests could lie either in how we built the SUT or in how we coded the tests themselves. Sometimes, the way the SUT was built forces us to write our tests in a way that makes them slow. This is particularly a problem with legacy code or code that was built with a "test last" perspective.

原因:组件使用缓慢

SUT 的一个组件具有较高的延迟。

A component of the SUT has high latency.

根本原因

测试速度慢的最常见原因是许多测试都与数据库交互。必须写入数据库以设置夹具并读取数据库以验证结果的测试(一种后门操纵;参见第 327页)的运行时间比针对内存数据结构运行的相同测试长 50 倍左右。这是使用慢速组件的更普遍问题的一个例子。

The most common cause of Slow Tests is interacting with a database in many of the tests. Tests that have to write to a database to set up the fixture and read a database to verify the outcome (a form of Back Door Manipulation; see page 327) take about 50 times longer to run than the same tests that run against in-memory data structures. This is an example of the more general problem of using slow components.

可能的解决方案

我们可以通过将慢速组件替换为提供近乎即时响应的测试替身第 522页)来提高测试运行速度。当慢速组件是数据库时,使用伪数据库(请参阅伪对象)可以使测试运行速度平均提高 50 倍!请参阅第 319页的侧栏“不使用共享装置加快测试速度” ,了解其他解决方法。

We can make our tests run much faster by replacing the slow components with a Test Double (page 522) that provides near-instantaneous responses. When the slow component is the database, the use of a Fake Database (see Fake Object) can make the tests run on average 50 times faster! See the sidebar "Faster Tests Without Shared Fixtures" on page 319 for other ways to skin this cat.

原因:通用装置
症状

测试总是很慢,因为每个测试都构建相同的过度设计的装置。

Tests are consistently slow because each test builds the same over-engineered fixture.

根本原因

每次构建Fresh Fixture (第 311页) 时,每个测试都会构建一个大型General Fixture。由于General Fixture包含的对象比Minimal Fixture (第302页)多得多,因此构建它自然需要更长的时间。Fresh Fixture涉及为每个Testcase Object (第 382页)设置一个全新的 Fixture 实例,因此将“更长”乘以测试数量即可了解速度下降的程度!

Each test constructs a large General Fixture each time a Fresh Fixture (page 311) is built. Because a General Fixture contains many more objects than a Minimal Fixture (page 302), it naturally takes longer to construct. Fresh Fixture involves setting up a brand-new instance of the fixture for each Testcase Object (page 382), so multiply "longer" by the number of tests to get an idea of the magnitude of the slowdown!

可能的解决方案

我们的第一个倾向通常是将通用装置实现为共享装置,以避免为每个测试重建它。然而,除非我们能让这个共享装置不可变,否则这种方法很可能会导致不稳定的测试,应该避免。更好的解决方案是减少每个测试执行的装置设置量。

Our first inclination is often to implement the General Fixture as a Shared Fixture to avoid rebuilding it for each test. Unless we can make this Shared Fixture immutable, however, this approach is likely to lead to Erratic Tests and should be avoided. A better solution is to reduce the amount of fixture setup performed by each test.

原因:异步测试
症状

一些测试需要运行很长时间;这些测试包含明显的延迟。

A few tests take inordinately long to run; those tests contain explicit delays.

根本原因

测试方法中包含的延迟会大大减慢测试执行速度。当我们测试的软件产生线程或进程(异步代码;请参阅第 209页的难以测试的代码)并且测试需要等待它们启动、运行并验证它们预期会产生的任何副作用时,这种缓慢的执行可能是必要的。由于这些线程或进程启动所需的时间各不相同,因此测试通常需要包含较长的延迟以防万一——也就是说,确保测试始终通过。以下是一个带有延迟的测试示例:

Delays included within a Test Method slow down test execution considerably. This slow execution may be necessary when the software we are testing spawns threads or processes (Asynchronous Code; see Hard-to-Test Code on page 209) and the test needs to wait for them to launch, run, and verify whatever side effects they were expected to have. Because of the variability in how long it takes for these threads or processes to be started, the test usually needs to include a long delay "just in case"—that is, to ensure it passes consistently. Here's an example of a test with delays:

public class RequestHandlerThreadTest extends TestCase {

      private static final int TWO_SECONDS = 3000;

      public void testWasInitialized_Async() = throws InterruptedException {

          // 设置

          RequestHandlerThread sut = new RequestHandlerThread();

          // 练习

          sut.start();

          // 验证

          Thread.sleep(TWO_SECONDS);

          assertTrue(sut.initializedSuccessfully());

    }



    public void testHandleOneRequest_Async()

                throws InterruptedException {

          // 设置

          RequestHandlerThread sut = new RequestHandlerThread();

          sut.start();

          // 练习

          enqueRequest(makeSimpleRequest());

          // 验证

          Thread.sleep(TWO_SECONDS);

          assertEquals(1, sut.getNumberOfRequestsCompleted());

          assertResponseEquals(makeSimpleResponse(), getResponse());

    }

}

public  class  RequestHandlerThreadTest  extends  TestCase  {

      private  static  final  int  TWO_SECONDS  =  3000;

      public  void  testWasInitialized_Async()  =  throws  InterruptedException  {

          //  Setup

          RequestHandlerThread  sut  =  new  RequestHandlerThread();

          //  Exercise

          sut.start();

          //    Verify

          Thread.sleep(TWO_SECONDS);

          assertTrue(sut.initializedSuccessfully());

    }



    public  void  testHandleOneRequest_Async()

                throws  InterruptedException  {

          //  Setup

          RequestHandlerThread  sut  =  new  RequestHandlerThread();

          sut.start();

          //  Exercise

          enqueRequest(makeSimpleRequest());

          //  Verify

          Thread.sleep(TWO_SECONDS);

          assertEquals(1,  sut.getNumberOfRequestsCompleted());

          assertResponseEquals(makeSimpleResponse(),  getResponse());

    }

}

 
影响

两秒钟的延迟似乎不是什么大问题。但想想看,当我们有十几个这样的测试时会发生什么:运行这些测试将需要近半分钟的时间。相比之下,我们每秒可以运行数百个正常测试。

A two-second delay might not seem like a big deal. But consider what happens when we have a dozen such tests: It would take almost half a minute to run these tests. In contrast, we can run several hundred normal tests each second.

可能的解决方案

解决这个问题的最佳方法是通过同步测试逻辑来避免测试中的异步性。这可能需要我们进行提取可测试组件(第767页)重构以实现Humble 可执行文件(请参阅第695页的Humble 对象)。

The best way to address this problem is to avoid asynchronicity in tests by testing the logic synchronously. This may require us to do an Extract Testable Component (page 767) refactoring to implement a Humble Executable (see Humble Object on page 695).

原因:测试过多
症状

测试数量非常多,无论执行速度有多快,都必然需要很长时间才能运行。

There are so many tests that they are bound to take a long time to run regardless of how fast they execute.

根本原因

造成这一问题的明显原因是测试数量太多。也许我们的系统太庞大,大量测试确实有必要,或者也许测试之间重叠太多。

The obvious cause of this problem is having so many tests. Perhaps we have such a large system that the large number of tests really is necessary, or perhaps we have too much overlap between tests.

不太明显的原因是我们运行的测试太多太频繁了!

The less obvious cause is that we are running too many of the tests too frequently!

可能的解决方案

我们不必一直运行所有测试 !关键是要确保所有测试都定期运行。如果整个套件运行时间太长,请考虑创建一个包含合适测试横截面的子集套件(请参阅第 592页的命名测试套件);在每次提交操作之前运行此子套件。其余测试可以定期运行,尽管频率较低,方法是将它们安排在夜间或其他方便的时间运行。有些人将这种技术称为“构建管道”。有关此想法和其他想法的更多信息,请参阅第319页的侧栏“无需共享装置即可加快测试速度 ” 。

We don't have to run all the tests all the time! The key is to ensure that all tests are run regularly. If the entire suite is taking too long to run, consider creating a Subset Suite (see Named Test Suite on page 592) with a suitable cross section of tests; run this subsuite before every commit operation. The rest of the tests can be run regularly, albeit less often, by scheduling them to run overnight or at some other convenient time. Some people call this technique a "build pipeline." For more on this and other ideas, see the sidebar "Faster Tests Without Shared Fixtures" on page 319.

如果系统规模很大,最好将其分解成多个相当独立的子系统或组件。这样,负责每个组件的团队就可以独立工作,并且只运行针对其自己组件的测试。其中一些测试应充当其他组件如何使用该组件的代理;如果接口契约发生变化,则必须保持最新状态。嗯,测试即文档(参见第23页);我喜欢它!一些将所有组件一起执行的端到端测试(可能是故事测试的一种形式)是必不可少的,但它们不需要包含在预提交套件中。

If the system is large in size, it is a good idea to break it into a number of fairly independent subsystems or components. This allows teams working on each component to work independently and to run only those tests specific to their own component. Some of those tests should act as proxies for how the other components would use the component; they must be kept up-to-date if the interface contract changes. Hmmm, Tests as Documentation (see page 23); I like it! Some end-to-end tests that exercise all the components together (likely a form of storytests) would be essential, but they don't need to be included in the pre-commit suite.

第 17 章

项目异味

Chapter 17

Project Smells

 

本章中的气味

Smells in This Chapter

      

有缺陷的测试 260

      

Buggy Tests 260

      

开发人员不编写测试 263

      

Developers Not Writing Tests 263

      

测试维护成本高 265

      

High Test Maintenance Cost 265

      

生产错误 268

      

Production Bugs 268

有缺陷的测试

Buggy Tests

自动化测试中经常会发现错误。

Bugs are regularly found in the automated tests.

全自动测试(见第 26)应该充当进行迭代开发的团队的“安全网”。但我们如何才能确保安全网真正发挥作用呢?

Fully Automated Tests (see page 26) are supposed to act as a "safety net" for teams doing iterative development. But how can we be sure the safety net actually works?

有缺陷的测试是项目级别的迹象,表明我们的自动化测试并不完善。

Buggy Tests is a project-level indication that all is not well with our automated tests.

症状

Symptoms

构建失败,归咎于测试失败。经过仔细检查,我们发现测试的代码运行正常,但测试表明它有问题。

A build fails, and a failed test is to blame. Upon closer inspection, we discover that the code being testing works correctly, but the test indicated it was broken.

尽管我们进行了测试来验证发现错误的具体场景,但我们还是遇到了生产错误第 268页)。根本原因分析表明,测试中包含一个错误,导致无法捕获生产代码中的错误。

We encountered Production Bugs (page 268) despite having tests that verify the specific scenario in which the bug was found. Root-cause analysis indicates the test contains a bug that precluded catching the error in the production code.

影响

Impact

给出误导性结果的测试很危险!不该通过的测试(假阴性,如“这里没有问题”)会给人一种虚假的安全感。不该失败的测试(假阳性)会使测试失去可信度。它们就像那个喊着“狼来了!”的小男孩;发生几次之后,我们往往会忽略它们。

Tests that give misleading results are dangerous! Tests that pass when they shouldn't (a false negative, as in "nothing wrong here") give a false sense of security. Tests that fail when they shouldn't (a false positive) discredit the tests. They are like the little boy who cried, "Wolf!"; after a few occurrences, we tend to ignore them.

原因

Causes

测试中存在错误的原因有很多。大多数问题也表现为代码或行为异味。作为项目经理,我们不太可能发现这些潜在的异味,除非我们专门寻找它们。

Buggy Tests can have many causes. Most of these problems also show up as code or behavior smells. As project managers, we are unlikely to see these underlying smells until we specifically look for them.

原因:易碎测试

测试错误可能只是脆弱测试第 239在项目层面的症状。对于误报测试失败,一个好的起点是“四个敏感性”:接口敏感性(见脆弱测试)、行为敏感性(见脆弱测试)、数据敏感性(见脆弱测试上下文敏感性(见脆弱测试)。这些敏感性中的每一个都可能导致测试失败的变化。使用测试替身第 522)和重构来消除敏感性可能具有挑战性,但最终它将使测试更加可靠和具有成本效益。

Buggy Tests may just be project-level symptoms of a Fragile Test (page 239). For false-positive test failures, a good place to start is the "four sensitivities": Interface Sensitivity (see Fragile Test), Behavior Sensitivity (see Fragile Test), Data Sensitivity (see Fragile Test), and Context Sensitivity (see Fragile Test). Each of these sensitivities could be the change that caused the test to fail. Removing the sensitivities by using Test Doubles (page 522) and refactoring can be challenging but ultimately it will make the tests much more dependable and cost-effective.

原因:模糊的测试

导致假阴性测试结果(本不应该通过的测试却通过了)的一个常见原因是模糊测试(第 186页),这种测试很难正确执行,尤其是当我们修改因我们的更改而破坏的现有测试时。由于自动化测试很难测试,我们通常不会验证修改后的测试是否仍能捕获最初设计用于捕获的所有错误。只要我们看到绿色条,我们就会认为“一切顺利”。但实际上,我们可能创建了一个永远不会失败的测试。

A common cause of false-negative test results (tests that pass when they shouldn't) is an Obscure Test (page 186), which is difficult to get right—especially when we are modifying existing tests that were broken by a change we made. Because automated tests are hard to test, we don't often verify that a modified test still catches all the bugs it was initially designed to trap. As long as we see a green bar, we think we are "good to go." In reality, we may have created a test that never fails.

解决模糊测试的最佳方法是重构测试,将重点放在测试的读者身上。真正的目标是将测试作为文档(参见第23页)——任何低于这一目标的行为都会增加出现错误测试的可能性。

Obscure Tests are best addressed through refactoring of tests to focus on the reader of the tests. The real goal is Tests as Documentation (see page 23)—anything less will increase the likelihood of Buggy Tests.

原因:代码难以测试

测试有缺陷的另一个常见原因,尤其是对于“遗留软件”(即任何没有完整自动化测试套件的软件),是软件的设计不利于自动化测试。这种难以测试的代码(第 209页)可能迫使我们使用间接测试(参见模糊测试而这反过来又可能导致脆弱测试

Another common cause of Buggy Tests, especially with "legacy software" (i.e., any software that doesn't have a complete suite of automated tests), is that the design of the software is not conducive to automated testing. This Hard-to-Test Code (page 209) may force us to use Indirect Testing (see Obscure Test), which in turn may result in a Fragile Test.

让难以测试的代码变得易于测试的唯一方法是重构代码以提高其可测试性。(第 6 章测试自动化策略”第 11 章使用测试替身”中描述了这种转变。)如果这不可行,我们可以通过应用SUT API 封装来减少受更改影响的测试代码量(请参阅第 599页的测试实用程序方法)。

The only way Hard-to-Test Code will become easy to test is if we refactor the code to improve its testability. (This transformation is described in Chapter 6, Test Automation Strategy, and Chapter 11, Using Test Doubles.) If this is not an option, we may be able to reduce the amount of test code affected by a change by applying SUT API Encapsulation (see Test Utility Method on page 599).

故障排除建议

Troubleshooting Advice

当我们进行有缺陷的测试时,提出大量问题很重要。我们必须问“五个为什么” [TPS]来找出问题的根源 — 也就是说,我们必须准确确定哪些代码和/或行为异味导致了有缺陷的测试,并找出每种异味的根本原因。

When we have Buggy Tests, it is important to ask lots of questions. We must ask the "five why's" [TPS] to get to the bottom of the problem—that is, we must determine exactly which code and/or behavior smells are causing the Buggy Tests and find the root cause of each smell.

解决方案模式

Solution Patterns

解决方案很大程度上取决于测试出现错误的原因。请参阅底层行为和代码异味以找到可能的解决方案。

The solution depends very much on why the Buggy Tests occurred. Refer to the underlying behavior and code smells for possible solutions.

与所有“项目异味”一样,我们应该寻找项目层面的原因。其中包括没有给开发人员足够的时间来执行以下活动:

As with all "project smells," we should look for project-level causes. These include not giving developers enough time to perform the following activities:

  • 学习正确地编写测试
  • Learn to write the tests properly
  • 重构遗留代码,使测试自动化更容易、更强大
  • Refactor the legacy code to make test automation easier and more robust
  • 首先编写测试
  • Write the tests first

如果不解决这些项目层面的原因,问题肯定会在不久的将来再次出现。

Failure to address these project-level causes guarantees that the problems will recur in the near future.

开发人员不编写测试

Developers Not Writing Tests

开发人员没有编写自动化测试。

Developers aren't writing automated tests.

症状

Symptoms

我们听说我们的开发人员没有编写测试。或者我们可能观察到了生产错误第 268页)并问:“为什么有这么多错误?”,结果却被告知:“因为我们没有编写测试来覆盖软件的这一部分。”

We hear that our developers aren't writing tests. Or maybe we have observed Production Bugs (page 268) and asked, "Why are so many bugs getting through?", only to be told, "Because we aren't writing tests to cover that part of the software."

影响

Impact

如果团队没有为“可能出错”的每一个软件编写自动化测试,那么它就等于在抵押自己的未来。目前的软件开发速度将无法长期持续,因为系统将背负测试债务。添加新功能将花费越来越长的时间,而重构代码以改进其设计将充满危险(因此这种情况将越来越少发生)。这个问题标志着走向传统偏执、非敏捷开发的“滑坡”的开始。如果这就是我们渴望达到的目标,我们就应该坚持下去。否则,是时候采取行动了。

If the team isn't writing automated tests for every piece of software "that could possibly break," it is mortgaging its future. The current pace of software development will not be sustainable over the long haul because the system will be in test debt. It will take longer and longer to add new functionality, and refactoring the code to improve its design will be fraught with peril (so it will happen less and less frequently). This problem marks the beginning of a trip down the proverbial "slippery slope" to traditional paranoid, non-agile development. If that is where we aspire to be, we should stay the course. Otherwise, it is time to take action.

原因

Causes

原因:时间不够

开发人员可能难以在规定的开发时间内编写测试。这个问题可能是由于开发计划过于紧张或主管/团队领导指示开发人员“不要浪费时间编写测试”造成的。或者,开发人员可能不具备有效编写测试所需的技能,并且可能没有分配到所需的时间来提高学习能力。

Developers may have trouble writing tests in the time they are given to do the development. This problem could be caused by an overly aggressive development schedule or supervisors/team leaders who instruct developers, "Don't waste time writing tests." Alternatively, developers may not have the skills needed to write tests efficiently and may not be allocated the time required to work their way up the learning curve.

如果开发人员需要时间,那么管理人员需要调整项目进度表,以便给他们时间。这种延长只是暂时的调整,开发人员需要学习技能和测试自动化基础设施,以便能够更快地编写测试。根据我的经验,一旦开发人员内化了该流程,他们就可以在编写和调试代码所花费的相同时间内编写测试和代码。编写测试所花费的时间被未花在调试器上的时间所抵消。

If time is what the developers need, managers need to adjust the project schedule to give them that time. This extension need be only a temporary adjustment while the developers learn the skills and test automation infrastructure that will enable them to write the tests more quickly. In my experience, once developers have internalized the process, they can write the tests and the code in the same amount of time it once took them to write and debug just the code. The time spent writing the tests is more than compensated for by the time not spent in the debugger.

原因:代码难以测试

开发人员不编写测试的一个常见原因是,特别是对于“遗留软件”(即任何没有完整自动化测试套件的软件),软件的设计不利于自动化测试。这种情况在其自身的问题“难以测试的代码”(第209页)中有更详细的描述。

A common cause of Developers Not Writing Tests, especially with "legacy software" (i.e., any software that doesn't have a complete suite of automated tests), is that the design of the software is not conducive to automated testing. This situation is described in more detail in its own smell, Hard-to-Test Code (page 209).

原因:错误的测试自动化策略

开发人员不编写测试的另一个原因可能是测试环境或测试自动化策略导致编写时间过长的脆弱测试第 239页)或模糊测试(第186页)。我们需要问“五个为什么” [TPS]来找到根本原因。然后我们可以解决这些原因并让事情回到正轨。

Another cause of Developers Not Writing Tests may be a test environment or test automation strategy that leads to Fragile Tests (page 239) or Obscure Tests (page 186) that take too long to write. We need to ask the "five why's" [TPS] to find the root causes. Then we can address those causes and get the ship back on course.

故障排除建议

Troubleshooting Advice

项目级异味(例如开发人员不编写测试)更有可能被项目经理、Scrum 主管或团队领导发现,而不是被开发人员发现。作为管理者,我们可能不知道如何解决问题,但我们对问题的认识和认知才是最重要的。这种独特的视角使管理者可以向开发团队询问他们为什么不编写测试、在什么情况下不编写测试以及编写测试需要多长时间。然后管理者可以鼓励和授权开发人员想出解决根本原因的方法,以便他们编写所有必要的测试。

Project-level smells such as Developers Not Writing Tests are more likely to be detected by a project manager, scrum master, or team leader than by a developer. As managers, we may not know how to fix the problem, but our awareness and recognition of it is what matters. This unique perspective allows managers to ask the development team questions about why they aren't writing tests, in which circumstances, and how long it takes to write tests when they do so. Then managers can encourage and empower the developers to come up with ways of addressing the root causes so that they write all the necessary tests.

当然,管理人员必须全力支持开发人员实施他们提出的任何改进计划。这种支持必须包括足够的时间来学习必要的技能并构建或设置必要的测试基础设施。管理人员不应该指望事情一夜之间就能好转。他们可以为每次迭代设定一个流程改进目标,例如“减少 20% 的未测试代码”或“提高 20% 的代码覆盖率”。这些目标应该合理且足够高,以鼓励正确的行为,而不仅仅是让数字看起来不错。(例如,只需将测试分成更小的部分或克隆测试,就可以实现编写 205 个测试的目标,而无需增加一丝测试覆盖率。)

Of course, managers must give the developers their full support in carrying out whatever improvement plan they come up with. That support must include enough time to learn the requisite skills and build or set up the necessary test infrastructure. And managers shouldn't expect things to turn around overnight. They might set a process improvement goal for each iteration, such as "20% reduction in code not tested" or "20% improvement in code coverage." These goals should be reasonable and at a high-enough level that they encourage the right behavior, as opposed to just making the numbers look good. (A goal of 205 more tests written, for example, could be achieved without increasing the test coverage one iota simply by splitting tests into smaller pieces or cloning tests.)

测试维护成本高

High Test Maintenance Cost

维护现有测试花费了太多精力。

Too much effort is spent maintaining existing tests.

测试代码需要与它所验证的生产代码一起维护。随着应用程序的发展,每当我们更改 SUT 类以添加新功能或每当我们重构测试以简化这些类时,我们可能都必须定期重新审视我们的测试。当测试变得过于难以理解和维护时,就会产生高测试维护 成本。

Test code needs to be maintained along with the production code it verifies. As an application evolves, we will likely have to revisit our tests on a regular basis whenever we change the SUT classes to add new functionality or whenever we refactor the tests to simplify those classes. High Test Maintenance Cost occurs when the tests become overly difficult to understand and maintain.

症状

Symptoms

新功能的开发速度变慢。每次我们添加一些新功能时,我们都需要对现有测试进行大量更改。开发人员或测试自动化人员可能会告诉项目经理或教练,他们需要“测试重构/清理迭代”。

Development of new functionality slows down. Every time we add some new functionality, we need to make extensive changes to the existing tests. Developers or test automaters may tell the project manager or coach that they need a "test refactoring/cleanup iteration."

如果我们一直在跟踪编写新测试和修改现有测试所花费的时间以及执行代码以使测试通过所花费的时间,我们会注意到大部分时间都花在修改现有测试上。

If we have been tracking the amount of time we spend writing the new tests and modifying existing tests separately from the time we spend implementing the code to make the tests pass, we notice that most of the time is spent modifying the existing tests.

大多数测试可维护性问题都伴随着其他问题,例如:

Most test maintainability issues are accompanied by other smells, such as the following:

影响

Impact

由于测试需要花费大量精力来维护,团队生产力会大幅下降。开发人员可能会鼓动“停止运行”(从测试套件中删除受影响的测试)。虽然编写生产代码是强制性的,但维护测试完全是可选的(至少对不知情的人来说是这样)。如果不解决这个问题,当团队或管理层决定测试自动化“不起作用”并放弃测试时,整个测试自动化工作可能会白费。

Team productivity drops significantly because the tests take so much effort to maintain. Developers may be agitating to "cut and run" (remove the affected tests from the test suites). While writing the production code is mandatory, maintaining the tests is completely optional (at least to the uninformed). If nothing is done about this problem, the entire test automation effort may be wasted when the team or management decides that test automation just "doesn't work" and abandons the tests.

原因

Causes

测试维护成本过高的根本原因是未能注意第 5 章测试自动化原则”中描述的原则。更直接的原因通常是测试代码重复过多(第 213页)以及测试与 SUT 的 API 耦合过于紧密。

The root cause of High Test Maintenance Cost is failing to pay attention to the principles described in Chapter 5, Principles of Test Automation. A more immediate cause is often too much Test Code Duplication (page 213) and tests that are too closely coupled to the API of the SUT.

原因:易碎测试

由于对 SUT 进行了微小更改而失败的测试称为脆弱测试。它们会导致高昂的测试维护成本,因为在进行各种实际上不应该影响它们的微小更改后,它们需要重新进行检查和“嘲笑”。

Tests that fail because minor changes were made to the SUT are called Fragile Tests. They result in High Test Maintenance Cost because they need to be revisited and "giggled" after all manner of minor changes that really shouldn't affect them.

导致失败的根本原因可能是“四种敏感性”中的任何一种:接口敏感性(参见脆弱性测试)、行为敏感性(参见脆弱性测试)、数据敏感性(参见脆弱性测试上下文敏感性(参见脆弱性测试)。我们可以通过使用测试替身第 522页)来保护测试免受尽可能多的这些敏感性的影响,并将系统重构为可以单独测试的较小组件和类,从而降低高测试维护成本。

The root cause of this failure can be any of the "four sensitivities": Interface Sensitivity (see Fragile Test), Behavior Sensitivity (see Fragile Test), Data Sensitivity (see Fragile Test) , and Context Sensitivity (see Fragile Test). We can reduce the High Test Maintenance Cost by protecting the tests against as many of these sensitivities as possible through the use of Test Doubles (page 522) and by refactoring the system into smaller components and classes that can be tested individually.

原因:模糊的测试

模糊测试第 186页)是导致测试维护成本过高的主要原因,因为每次访问时都需要花费更长的时间才能理解。当需要修改时,需要花费更多精力进行调整,而且“第一次就成功”的可能性更小,从而导致需要对测试进行更多调试。模糊测试也更有可能最终无法捕获它们想要检测的情况,从而导致出现“错误测试”(第260)。

Obscure Tests (page 186) are a major contributor to High Test Maintenance Cost because they take longer to understand each time they are visited. When they need to be modified, they take more effort to adjust and are much less likely to "work the first time," resulting in more debugging of tests. Obscure Tests are also more likely to end up not catching conditions they were intended to detect, which can lead to Buggy Tests (page 260).

解决模糊测试的最佳方法是重构测试,将重点放在测试的读者身上。真正的目标是将测试作为文档(参见第23页)——任何低于这一目标的行为都会增加高测试维护成本的可能性。

Obscure Tests are best addressed by refactoring tests to focus on the reader of the tests. The real goal is Tests as Documentation (see page 23)—anything less will increase the likelihood of High Test Maintenance Cost.

原因:代码难以测试

“遗留软件”(即任何没有完整自动化测试套件的软件)可能难以测试,因为我们通常在“最后”编写测试(在软件已经存在之后)。如果软件的设计不利于自动化测试,我们可能被迫通过涉及大量意外复杂性的笨拙界面使用间接测试(参见模糊测试);这种努力可能会导致脆弱测试

"Legacy software" (i.e., any software that doesn't have a complete suite of automated tests) can be hard to test because we typically write the tests "last" (after the software already exists). If the design of the software is not conducive to automated testing, we may be forced to use Indirect Testing (see Obscure Test) via awkward interfaces that involve a lot of accidental complexity; that effort may result in Fragile Tests.

重构代码以提高其可测试性需要花费时间和精力。然而,如果它们能消除高测试维护成本,那么这些时间和精力就是值得的。如果重构不是一种选择,我们可以通过执行SUT API 封装(请参阅第599页的测试实用程序方法)来减少受更改影响的测试代码量。例如,创建方法415页)封装了构造函数,从而使测试不易受到构造函数签名或语义变化的影响。

It will take both time and effort to refactor the code to improve its testability. Nevertheless, that time and effort are well spent if they eliminate the High Test Maintenance Cost. If refactoring is not an option, we may be able to reduce the amount of test code affected by a change by doing SUT API Encapsulation (see Test Utility Method on page 599) using Test Utility Methods. For example, Creation Methods (page 415) encapsulate the constructors, thereby rendering the tests less susceptible to changes in constructor signatures or semantics.

故障排除建议

Troubleshooting Advice

作为项目级问题,高测试维护成本很容易被项目经理、Scrum 主管或团队领导发现,就像被开发人员发现一样。虽然管理人员可能不具备解决问题​​和修复问题所需的技术深度,但他们意识到这一点非常重要。这种意识使管理人员可以询问开发团队维护测试需要多长时间、测试维护的频率以及为什么有必要。然后,经理可以要求开发人员找到更好的方法——一种不会导致如此高测试维护成本的方法

As a project-level smell, High Test Maintenance Cost is as likely to be detected by a project manager, scrum master, or team leader as by a developer. While managers may not have the technical depth needed to troubleshoot and fix the problem, the fact that they become aware of it is what is important. This awareness allows the manager to question the development team about how long it is taking to maintain tests, how often test maintenance occurs, and why it is necessary. Then the manager can challenge the developers to find a better way—one that won't result in such High Test Maintenance Costs!

当然,开发人员需要经理的支持才能实施他们提出的任何改进计划。这种支持必须包括进行调查(峰值)的时间、学习/培训的时间以及实际工作的时间。经理可以通过“测试重构故事”、调整速度以减少向客户承诺的新功能或其他方式来为这项活动腾出时间。无论经理如何分配这段时间,他们都必须记住,如果他们现在不给开发团队解决问题所需的资源,那么当团队的测试数量增加一倍时,问题只会变得更糟,并且在未来更难修复。

Of course, the developers will need the manager's support to carry out whatever improvement plan they come up with. That support must include time to conduct the investigations (spikes), learning/training time, and time to do the actual work. Managers can make time for this activity by having "test refactoring stories," adjusting the velocity to reduce the new functionality committed to the customer, or other means. Regardless of how managers carve out this time, they must remember that if they don't give the development team the resources needed to fix the problem now, the problem will simply get worse and become even more challenging to fix in the future when the team has twice as many tests.

生产错误

Production Bugs

我们在正式测试或生产过程中发现了太多错误。

We find too many bugs during formal tests or in production.

症状

Symptoms

我们在编写自动化测试方面投入了大量精力,但在正式(即系统)测试或生产中出现的错误数量仍然太高。

We have put a lot of effort into writing automated tests, yet the number of bugs showing up in formal (i.e., system) testing or production remains too high.

影响

Impact

排除和修复在正式测试中发现的错误所需的时间比在开发中发现的错误所需的时间更长,而排除和修复在生产中发现的错误所需的时间甚至更长。我们可能被迫推迟产品发货或将应用程序投入生产,以便有时间修复错误和重新测试。这些时间和精力会直接转化为金钱成本,并消耗本可用于为产品添加更多功能或构建其他产品的资源。这种延迟还可能损害组织在客户眼中的信誉。质量差也会产生间接成本,因为它会降低我们提供的产品或服务的价值。

It takes longer to troubleshoot and fix bugs found in formal testing than those found in development, and even longer to troubleshoot and fix bugs found in production. We may be forced to delay shipping the product or putting the application into production to allow time for the bug fixes and retesting. This time and effort translate directly into monetary costs and consume resources that might otherwise be used to add more functionality to the product or to build other products. The delay may also damage the organization's credibility in the eyes of its customers. Poor quality has an indirect cost as well, in that it lowers the value of the product or service we are supplying.

原因

Causes

Bug 可能由于多种原因而漏到生产中,包括不经常运行的测试未经测试的代码。后者可能是由于缺少单元测试丢失测试造成的。

Bugs may slip through to production for several reasons, including Infrequently Run Tests or Untested Code. The latter problem may result from Missing Unit Tests or Lost Tests.

通过指定运行“足够的测试”,我们的意思是测试覆盖率应该足够,而不是必须执行特定数量的测试。对未经测试的代码的更改更有可能导致生产错误,因为没有自动化测试来告诉开发人员何时引入了问题。每次运行测试时都不会验证未经测试的需求,因此我们无法确定哪些是有效的。这两个问题都与开发人员不编写测试第 263页)有关。

By specifying that "enough tests" be run, we mean the test coverage should be adequate, rather than that some specific number of tests must be carried out. Changes to Untested Code are more likely to result in Production Bugs because there are no automated tests to tell the developers when they have introduced problems. Untested Requirements aren't being verified every time the tests are run, so we don't know for sure what is working. Both of these problems are related to Developers Not Writing Tests (page 263).

原因:不经常运行测试
症状

我们听说我们的开发人员很少运行测试。当我们提出一些问题时,我们发现运行测试花费的时间太长(慢速测试;参见第 253页)或产生太多无关故障(错误测试;参见第260页)。

We hear that our developers aren't running the tests very often. When we ask some questions, we discover that running the tests takes too long (Slow Tests; see page 253) or produces too many extraneous failures (Buggy Tests; see page 260).

我们在日常的集成构建 [SCM] 中看到测试失败的情况。深入挖掘后,我们发现开发人员经常提交代码而不在自己的机器上运行测试。

We see test failures in the daily Integration Build [SCM]. When we dig deeper, we find that developers often commit their code without running the tests on their own machines.

根本原因

一旦他们看到了使用自动化测试安全网的好处,大多数开发人员将继续使用这些测试,除非出现任何阻碍。最常见的障碍是缓慢测试,这会减慢预集成回归测试的速度,或不可重复的测试(请参阅第 228页的不稳定测试),迫使开发人员在运行测试之前重新启动测试环境或进行手动干预(第250页)。

Once they've seen the benefits of working with the safety net of automated tests, most developers will continue using these tests unless something gets in the way. The most common impediments are Slow Tests that slow down the pre-integration regression testing or Unrepeatable Tests (see Erratic Test on page 228) that force developers to restart their test environment or do Manual Intervention (page 250) before running the tests.

可能的解决方案

如果根本原因是不可重复的测试,我们可以尝试切换到Fresh Fixture (第 311页) 策略,以使测试更具确定性。如果原因是测试速度慢,我们必须付出更多努力来加快测试运行速度。

If the root cause is Unrepeatable Tests, we can try switching to a Fresh Fixture (page 311) strategy to make the tests more deterministic. If the cause is Slow Tests, we must put more effort into speeding up the test run.

原因:测试失败
症状

测试套件中执行的测试数量有所下降(或没有像预期的那样增加)。如果我们关注测试数量,我们可能会直接注意到这一点。或者,我们可能会发现一个错误,该错误本应由我们知道存在的测试引起,但在仔细研究后,我们发现该测试已被禁用。

The number of tests being executed in a test suite has declined (or has not increased as much as expected). We may notice this directly if we are paying attention to test counts. Alternatively, we may find a bug that should have been caused by a test that we know exists but, upon poking around, we discover that the test has been disabled.

根本原因

丢失的测试可能是由于测试方法(第348页) 或测试用例类(第 373页) 导致的,因为它们已被禁用或从未添加到AllTests 套件(请参阅第 592页的命名测试套件)。

Lost Tests can be caused by either a Test Method (page 348) or a Testcase Class (page 373) that has been disabled or has never been added to the AllTests Suite (see Named Test Suite on page 592).

在下列情况下,测试可能会被意外地遗漏(即从未运行)在测试套件之外:

Tests can be accidentally left out (i.e., never run) of test suite in the following circumstances:

过去运行的测试可能已通过以下任一方式被禁用:

Tests that ran in the past may have been disabled in any of the following ways:

  • 我们重命名了测试方法,使其与导致测试发现将测试包含在测试套件中的模式不匹配(例如,方法名称以“test . . . ”开头)。
  • We renamed the Test Method to not match the pattern that causes Test Discovery to include the test in the test suite (e.g., the method name starts with "test . . .").
  • 我们[Ignore]在 xUnit 的变体中添加了一个属性,使用方法属性来指示测试方法
  • We added an [Ignore] attribute in variants of xUnit that use method attributes to indicate Test Methods.
  • 我们注释掉(或删除)了明确将测试(或套件)添加到套件的代码。
  • We commented out (or deleted) the code that adds the test (or suite) to the suite explicitly.

通常,当某项测试失败时,有人会禁用该测试,以避免在运行其他测试时不得不处理失败的测试,这时就会发生丢失测试。当然,这种情况也可能是意外发生的。

Typically, a Lost Test occurs when a test is failing and someone disables it to avoid having to wade through the failing tests when running other tests. It may also occur accidentally, of course.

可能的解决方案

有多种方法可以避免引入丢失测试

There are a number of ways to avoid introducing Lost Tests.

我们可以使用单个测试套件(请参阅命名测试套件)来运行单个测试方法,而不是禁用失败或缓慢的测试。我们可以使用测试树资源管理器(请参阅第 377页的测试运行器)深入挖掘并运行测试套件中的单个测试。这两种技术都因链式测试第 454页)而变得困难——链式测试是一种刻意的交互测试形式(请参阅不稳定测试),所以这只是避免使用它们的又一个原因。

We can use a Single Test Suite (see Named Test Suite) to run a single Test Method instead of disabling the failing or slow test. We can use the Test Tree Explorer (see Test Runner on page 377) to drill down and run a single test from within a test suite. Both of these techniques are made difficult by Chained Tests (page 454)—a deliberate form of Interacting Tests (see Erratic Test)—so this is just one more reason to avoid them.

如果我们的 xUnit 变体支持此功能,我们可以使用提供的机制来忽略1 个测试。它通常会提醒我们未运行的测试数量,以便我们不会忘记重新启用它们。我们还可以配置我们的持续集成工具,如果“忽略”的测试数量超过某个阈值,则构建失败。

If our variant of xUnit supports it, we can use the provided mechanism to ignore1 a test. It will typically remind us of the number of tests not being run so we don't forget to re-enable them. We can also configure our continuous integration tool to fail the build if the number of tests "ignored" exceeds a certain threshold.

我们可以将签入后的测试数量与开始集成之前代码分支中存在的测试数量进行比较。我们只需验证此计数是否增加了我们添加的测试数量。

We can compare the number of tests we have after check-in with the number of tests that existed in the code branch immediately before we started integration. We simply verify that this count has increased by the number of tests we have added.

如果我们的编程语言支持反射,我们就可以实现或利用测试发现。

We can implement or take advantage of Test Discovery if our programming language supports reflection.

我们可以使用不同的策略来查找要在 Integration Build 中运行的测试。某些构建工具(例如 Ant)允许我们查找所有符合名称模式的文件(例如以“Test”结尾的文件)。如果我们使用此功能来获取所有测试,我们就不会丢失整个测试套件。

We can use a different strategy for finding the tests to run in the Integration Build. Some build tools (such as Ant) let us find all files that match a name pattern (e.g., those ending in "Test"). We won't lose entire test suites if we use this capability to pick up all the tests.

原因:缺少单元测试
症状

所有单元测试都通过了,但客户测试仍然失败。在某个时候,客户测试通过了,但没有编写单元测试来验证各个类的行为。然后,后续的代码更改修改了其中一个类的行为,从而破坏了其功能。

All the unit tests pass but a customer test continues to fail. At some point, the customer test passed—but no unit tests were written to verify the behavior of the individual classes. Then, a subsequent code change modified the behavior of one of the classes, which broke its functionality.

根本原因

当团队专注于编写客户测试,但未能使用单元测试进行测试驱动开发时,通常会发生缺少单元测试的情况。团队成员可能已经构建了足够的功能来通过客户测试,但随后的重构破坏了它。单元测试可能会阻止代码更改进入集成构建。

Missing Unit Tests often happen when a team focuses on writing the customer tests but fails to do test-driven development using unit tests. The team members may have built enough functionality to pass the customer tests, but a subsequent refactoring broke it. Unit tests would likely have prevented the code change from reaching the Integration Build.

在测试驱动开发过程中,当开发人员超越自己并编写一些代码而没有失败的测试来指导他们时,也会出现缺少单元测试的情况。

Missing Unit Tests can also arise during test-driven development when developers get ahead of themselves and write some code without having a failing test to guide them.

可能的解决方案

最老套的答案是编写更多单元测试。当然,说起来容易做起来难,而且并不总是有效。进行真正的测试驱动开发是避免缺少单元测试的最佳方法,而不必仅仅为了增加测试数量而编写不必要的测试。

The trite answer is to write more unit tests. Of course, this is easier said than done, and it isn't always effective. Doing true test-driven development is the best way to avoid having Missing Unit Tests without writing unnecessary tests merely to get the test count up.

原因:未经测试的代码
症状

我们可能只是“知道”SUT 中的某些代码段未被任何测试执行。也许我们从未见过该代码执行,或者也许我们使用代码覆盖工具来毫无疑问地证明这一事实。在下面的示例中,我们如何测试当timeProvider抛出异常时,该异常是否得到正确处理?

We may just "know" that some piece of code in the SUT is not being exercised by any tests. Perhaps we have never seen that code execute, or perhaps we used code coverage tools to prove this fact beyond a doubt. In the following example, how can we test that when timeProvider throws an exception, this exception is handled correctly?

public String getCurrentTimeAsHtmlFragment()

        throws TimeProviderEx {

     Calendar currentTime;

     try {

         currentTime = getTimeProvider().getTime();

     } catch (Exception e) {

         return e.getMessage();

     }

     // 等等

public  String  getCurrentTimeAsHtmlFragment()

        throws  TimeProviderEx  {

     Calendar  currentTime;

     try  {

         currentTime  =  getTimeProvider().getTime();

     }  catch  (Exception  e)  {

         return  e.getMessage();

     }

     //  etc.

 
根本原因

未测试代码最常见的原因是 SUT 包含对依赖组件 (DOC) 行为的特定方式作出反应的代码路径,而我们尚未找到执行这些路径的方法。通常,DOC 被同步调用,并返回特定值或抛出异常。在正常测试期间,实际上只会遇到间接输入的可能等价类的子集。

The most common cause of Untested Code is that the SUT includes code paths that react to particular ways that a depended-on component (DOC) behaves and we haven't found a way to exercise those paths. Typically, the DOC is being called synchronously and either returns certain values or throws exceptions. During normal testing, only a subset of the possible equivalence classes of indirect inputs are actually encountered.

未经测试的代码的另一个常见原因是由于通过 SUT 接口公开的功能特征描述不完整而导致测试套件不完整。

Another common cause of Untested Code is incompleteness of the test suite caused by incomplete characterization of the functionality exposed via the SUT's interface.

可能的解决方案

如果未测试代码是由于无法控制 SUT 的间接输入而导致的,最常见的解决方案是使用测试桩第 529页)将各种间接输入输入到 SUT 中以覆盖所有代码路径。否则,配置 DOC 以使其返回全面测试 SUT 所需的各种间接输入可能就足够了。

If the Untested Code is caused by an inability to control the indirect inputs of the SUT, the most common solution is to use a Test Stub (page 529) to feed the various kinds of indirect inputs into the SUT to cover all the code paths. Otherwise, it may be sufficient to configure the DOC to cause it to return the various indirect inputs required to fully test the SUT.

原因:未经测试的需求
症状

我们可能只是“知道”某些功能尚未经过测试。或者,我们可能正在尝试测试某个软件,但看不到任何可以通过该软件的公共接口进行测试的可见功能。然而,我们编写的所有测试都通过了。

We may just "know" that some piece of functionality is not being tested. Alternatively, we may be trying to test a piece of software but cannot see any visible functionality that can be tested via the public interface of the software. All the tests we have written pass, however.

在进行测试驱动开发时,我们知道需要添加一些代码来处理需求。但是,我们无法找到一种方法来表达在完全自动化测试中记录操作的代码需求(参见第26页),例如:

When doing test-driven development, we know we need to add some code to handle a requirement. However, we cannot find a way to express the need for code to log the action in a Fully Automated Test (see page 26) such as this:

public void testRemoveFlight() throws Exception {

      // 设置

      FlightDto expectedFlightDto = createARegisteredFlight();

      FlightManagementFacade Facade =

              new FlightManagementFacadeImpl();

      // 练习

      Facade.removeFlight(expectedFlightDto.getFlightNumber());

      // 验证

      assertFalse("flight 在被移除后不应存在",

                      Facade.flightExists( expectedFlightDto.

                                                                getFlightNumber()));

}

public  void  testRemoveFlight()  throws  Exception  {

      //  set  up

      FlightDto  expectedFlightDto  =  createARegisteredFlight();

      FlightManagementFacade  facade  =

              new  FlightManagementFacadeImpl();

      //  exercise

      facade.removeFlight(expectedFlightDto.getFlightNumber());

      //  verify

      assertFalse("flight  should  not  exist  after  being  removed",

                      facade.flightExists(  expectedFlightDto.

                                                                getFlightNumber()));

}

 

请注意,此测试不会验证是否已执行正确的日志记录操作。无论日志记录是否正确实现(甚至根本没有实现),它都会通过。以下是此测试正在验证的代码,其中包含未正确实现的 SUT 的间接输出:

Note that this test does not verify that the correct logging action has been done. It will pass regardless of whether the logging was implemented correctly—or even at all. Here's the code that this test is verifying, complete with the indirect output of the SUT that has not been implemented correctly:

public void removeFlight(BigDecimal flightNumber)

              throws FlightBookingException {

      System.out.println(" removeFlight("+flightNumber+")");

      dataAccess.removeFlight(flightNumber);

      logMessage("CreateFlight", flightNumber); // 错误!

}

public  void  removeFlight(BigDecimal  flightNumber)

              throws  FlightBookingException  {

      System.out.println("          removeFlight("+flightNumber+")");

      dataAccess.removeFlight(flightNumber);

      logMessage("CreateFlight",  flightNumber);  //  Bug!

}

 

如果我们计划依赖于logMessage在生产中维护应用程序时捕获的信息,那么我们如何确保它是正确的?显然,最好通过自动化测试来验证此功能。

If we plan to depend on the information captured by logMessage when maintaining the application in production, how can we ensure that it is correct? Clearly, it is desirable to have automated tests verify this functionality.

影响

被测系统的部分必需行为可能会被意外禁用,而不会导致任何测试失败。可能会将有缺陷的软件交付给客户。对引入缺陷的担忧可能会阻止无情的重构或删除被怀疑不需要的代码(即死代码)。

Part of the required behavior of the SUT could be accidentally disabled without causing any tests to fail. Buggy software could be delivered to the customer. The fear of introducing bugs could discourage ruthless refactoring or deletion of code suspected to be unneeded (i.e., dead code).

根本原因

未测试需求最常见的原因是 SUT 包含无法通过其公共接口看到的行为。它可能具有无法通过测试直接观察到的预期“副作用”(例如写出文件或记录或调用另一个对象或组件上的方法)——换句话说,它可能具有间接输出。

The most common cause of Untested Requirements is that the SUT includes behavior that is not visible through its public interface. It may have expected "side effects" that cannot be observed directly by the test (such as writing out a file or record or calling a method on another object or component)—in other words, it may have indirect outputs.

当 SUT 是整个应用程序时,未经测试的需求可能是由于没有完整的客户测试套件来验证 SUT 可见行为的所有方面。

When the SUT is an entire application, the Untested Requirement may be a result of not having a full suite of customer tests that verify all aspects of the visible behavior of the SUT.

可能的解决方案

如果问题在于缺少客户测试,我们需要至少编写足够的客户测试以确保所有组件都正确集成。这可能需要通过将表示层与业务逻辑层分离来提高应用程序的可测试性设计。

If the problem is missing customer tests, we need to write at least enough customer tests to ensure that all components are integrated properly. This may require improving the design-for-testability of the application by separating the presentation layer from the business logic layer.

当我们需要验证间接输出时,我们可以通过使用模拟对象(第544页)进行行为验证(第468页)。间接输出的测试在第 11 章“使用测试替身”中介绍。

When we have indirect outputs that we need to verify, we can do Behavior Verification (page 468) through the use of Mock Objects (page 544). Testing of indirect outputs is covered in Chapter 11, Using Test Doubles.

原因:永不失败测试
症状

我们可能只是“知道”某些功能无法正常工作,即使该功能的测试已通过。在进行测试驱动开发时,我们已为尚未编写的功能添加了测试,但我们不能让测试失败。

We may just "know" that some piece of functionality is not working, even though the tests for that functionality pass. When doing test-driven development, we have added a test for functionality we have not yet written but we cannot get the test to fail.

影响

如果即使不存在实现功能的代码,测试也不会失败,那么它对于缺陷定位(参见第22页) 有多大用处? 没多大用处!

If a test won't fail even when the code to implement the functionality doesn't exist, how useful is it for Defect Localization (see page 22)? Not very!

根本原因

此问题可能是由不正确的断言编码引起的,例如assertTrue(aVariable,  true) 而不是assertEquals(aVariable,  true) 或只是assertTrue(aVariable)。另一个原因更为险恶:当我们进行异步测试时,测试运行器可能无法看到或报告其他线程或进程中抛出的故障。

This problem can be caused by improperly coded assertions such as assertTrue(aVariable,  true) instead of assertEquals(aVariable,  true) or just assertTrue(aVariable). Another cause is more sinister: When we have asynchronous tests, failures thrown in the other thread or process may not be seen or reported by the Test Runner.

可能的解决方案

我们可以实现跨线程故障检测机制,以确保异步测试确实会失败。更好的解决方案是重构代码以支持Humble Executable(请参阅第695页的Humble Object)。

We can implement cross-thread failure detection mechanisms to ensure that asynchronous tests do, indeed, fail. An even better solution is to refactor the code to support a Humble Executable (see Humble Object on page 695).

第三部分

模式

Part III

The Patterns

 

第18章

测试策略模式

Chapter 18

Test Strategy Patterns

 

本章中的模式

Patterns in This Chapter

测试自动化策略

Test Automation Strategy

      

记录测试 278

      

Recorded Test 278

      

脚本测试 285

      

Scripted Test 285

      

数据驱动测试 288

      

Data-Driven Test 288

      

测试自动化框架 298

      

Test Automation Framework 298

测试夹具策略

Test Fixture Strategy

      

最小装置 302

      

Minimal Fixture 302

      

标准装置 305

      

Standard Fixture 305

      

新鲜装置 311

      

Fresh Fixture 311

      

共享装置 317

      

Shared Fixture 317

SUT 交互策略

SUT Interaction Strategy

      

后门操作 327

      

Back Door Manipulation 327

      

层测试 337

      

Layer Test 337

录制测试

Recorded Test

也称为

Also known as

录制和回放测试、机器人用户测试、捕获/回放测试

Record and Playback Test, Robot User Test, Capture/Playback Test

我们如何为我们的软件准备自动化测试?

How do we prepare automated tests for our software?

我们通过记录与应用程序的交互并使用测试工具回放它们来实现测试的自动化。

We automate tests by recording interactions with the application and playing them back using a test tool.

图像

自动化测试有多种用途。它们可用于软件更改后的回归测试。它们可以帮助记录软件的行为。它们可以在编写软件之前指定软件的行为。我们如何准备自动化测试脚本会影响它们的用途、它们对 SUT 更改的稳健性以及准备它们需要多少技能和精力。

Automated tests serve several purposes. They can be used for regression testing software after it has been changed. They can help document the behavior of the software. They can specify the behavior of the software before it has been written. How we prepare the automated test scripts affects which purposes they can be used for, how robust they are to changes in the SUT, and how much skill and effort it takes to prepare them.

记录测试使我们能够在 SUT 构建之后和更改之前快速创建回归测试。

Recorded Tests allow us to rapidly create regression tests after the SUT has been built and before it is changed.

工作原理

How It Works

我们使用一个工具来监控我们与 SUT 的交互。该工具会跟踪 SUT 向我们传达的大部分信息以及我们对 SUT 的响应。录制会话完成后,我们可以将会话保存到文件中以供以后播放。当我们准备好运行测试时,我们会启动该工具的“播放”部分并将其指向录制的会话。它会启动 SUT 并将我们录制的输入作为对 SUT 输出的响应。它还可能会将 SUT 的输出与录制会话期间的 SUT 响应进行比较。不匹配可能是测试失败的原因。

We use a tool that monitors our interactions with the SUT as we work with it. This tool keeps track of most of what the SUT communicates to us and our responses to the SUT. When the recording session is done, we can save the session to a file for later playback. When we are ready to run the test, we start up the "playback" part of the tool and point it at the recorded session. It starts up the SUT and feeds it our recorded inputs in response to the SUT's outputs. It may also compare the SUT's outputs with the SUT's responses during the recording session. A mismatch may be cause for failing the test.

一些录制测试工具允许我们调整工具对 SUT 在录制会话期间所说的内容和播放期间所说的内容进行比较的灵敏度。大多数录制测试工具通过用户界面与 SUT 进行交互。

Some Recorded Test tools allow us to adjust the sensitivity of the comparisons that the tool makes between what the SUT said during the recording session and what it said during the playback. Most Recorded Test tools interact with the SUT through the user interface.

何时使用它

When to Use It

一旦应用程序启动并运行,并且我们预计不会对其进行大量更改,我们就可以使用录制的测试进行回归测试。当现有应用程序需要重构(预期修改功能)并且我们没有脚本测试第 285页)可用作回归测试时,我们也可以使用录制的测试。生成一组录制的测试通常比为相同功能准备脚本测试要快得多。理论上,任何知道如何操作应用程序的人都可以进行测试录制;几乎不需要技术专业知识。实际上,许多商业工具的学习曲线都很陡峭。此外,可能需要一些技术专业知识来添加“检查点”,调整回放工具的灵敏度,或者在录制工具出现混乱并记录错误信息时调整测试脚本。

Once an application is up and running and we don't expect a lot of changes to it, we can use Recorded Tests to do regression testing. We could also use Recorded Tests when an existing application needs to be refactored (in anticipation of modifying the functionality) and we do not have Scripted Tests (page 285) available to use as regression tests. It is typically much quicker to produce a set of Recorded Tests than to prepare Scripted Tests for the same functionality. In theory, the test recording can be done by anyone who knows how to operate the application; very little technical expertise should be required. In practice, many of the commercial tools have a steep learning curve. Also, some technical expertise may be required to add "checkpoints," to adjust the sensitivity of the playback tool, or to adjust the test script if the recording tool became confused and recorded the wrong information.

大多数记录测试工具通过用户界面与 SUT 交互。如果 SUT 的用户界面在不断发展,这种方法会使它们特别容易变得脆弱(界面敏感性;请参阅第 239页的脆弱性测试)。即使是诸如更改按钮或字段的内部名称等小变化也足以导致回放工具出现故障。这些工具还倾向于以非常低和详细的级别记录信息,使测试难以理解(模糊测试第 186页);因此,如果它们因 SUT 的变化而中断,也很难手动修复。出于这些原因,如果 SUT 将继续发展,我们应该计划相当定期地重新记录测试。

Most Recorded Test tools interact with the SUT through the user interface. This approach makes them particularly prone to fragility if the user interface of the SUT is evolving (Interface Sensitivity; see Fragile Test on page 239). Even small changes such as changing the internal name of a button or field may be enough to cause the playback tool to stumble. The tools also tend to record information at a very low and detailed level, making the tests hard to understand (Obscure Test; page 186); as a result, they are also difficult to repair by hand if they are broken by changes to the SUT. For these reasons, we should plan on rerecording the tests fairly regularly if the SUT will continue to evolve.

如果我们想将测试用作文档(参见第 23页)或如果我们想使用测试来推动新开发,我们应该考虑使用脚本测试。这些目标很难通过商业记录测试工具实现,因为大多数工具不允许我们为测试记录定义高级语言(参见第41页)。可以通过在应用程序本身中构建记录测试功能或使用重构记录测试来解决此问题。

If we want to use the Tests as Documentation (see page 23) or if we want to use the tests to drive new development, we should consider using Scripted Tests. These goals are difficult to address with commercial Recorded Test tools because most do not let us define a Higher-Level Language (see page 41) for the test recording. This issue can be addressed by building the Recorded Test capability into the application itself or by using Refactored Recorded Test.

变体:重构记录测试

这两种策略的混合是使用“记录、重构、回放” 1序列从新记录的测试中提取一组“动作组件”或“动词” ,然后重新连接测试用例以调用这些“动作组件”,而不是使用详细的内联代码。 大多数商业捕获/重放工具都提供了将文字值(第 714页) 转换为可由主测试用例传递到“动作组件”的参数的方法。 当屏幕发生变化时,我们只需重新记录“动作组件”; 所有测试用例将继续运行,并自动使用新的“动作组件”定义。 此策略实际上与在单元测试中使用测试实用程序方法(第 599页) 与 SUT 交互相同。 它为在脚本测试中使用重构的记录测试组件作为高级语言打开了大门。 Mercury Interactive 的BPT 2等工具使用此范例以自上而下的方式编写测试脚本;一旦开发了高级脚本并指定了测试步骤所需的组件,更多技术人员就可以记录或手动编码各个组件。

A hybrid of the two strategies is to use the "record, refactor, playback"1 sequence to extract a set of "action components" or "verbs" from the newly Recorded Tests and then rewire the test cases to call these "action components" instead of having detailed in-line code. Most commercial capture/replay tools provide the means to turn Literal Values (page 714) into parameters that can be passed into the "action component" by the main test case. When a screen changes, we simply rerecord the "action component"; all the test cases continue to function by automatically using the new "action component" definition. This strategy is effectively the same as using Test Utility Methods (page 599) to interact with the SUT in unit tests. It opens the door to using the Refactored Recorded Test components as a Higher-Level Language in Scripted Tests. Tools such as Mercury Interactive's BPT2 use this paradigm for scripting tests in a top-down manner; once the high-level scripts are developed and the components required for the test steps are specified, more technical people can either record or hand-code the individual components.

实施说明

Implementation Notes

使用记录测试策略时,我们有两个基本选择:我们可以获取第三方工具来记录我们与应用程序交互时发生的通信,或者我们可以在我们的应用程序中构建“记录和回放”机制。

We have two basic choices when using a Recorded Test strategy: We can either acquire third-party tools that record the communication that occurs while we interact with the application or we can build a "record and playback" mechanism right into our application.

变体:外部测试记录

市场上有许多测试记录工具,每种工具都有自己的优点和缺点。最佳选择取决于应用程序的用户界面性质、我们的预算、要验证的功能的复杂性以及可能的其他因素。

Many test recording tools are available commercially, each of which has its own strengths and weaknesses. The best choice will depend on the nature of the user interface of the application, our budget, the complexity of the functionality to be verified, and possibly other factors.

如果我们想使用测试来推动开发,我们需要选择一个使用测试记录文件格式的工具,这种格式可以手动编辑,并且易于理解。我们需要手动编写内容——即使我们使用“记录和回放”工具来执行测试,这种情况实际上也是脚本测试的一个例子。

If we want to use the tests to drive development, we need to pick a tool that uses a test-recording file format that is editable by hand and easily understood. We'll need to handcraft the contents—this situation is really an example of a Scripted Test even if we are using a "record and playback" tool to execute the tests.

变体:内置测试记录

还可以在 SUT 中构建记录测试功能。在这种情况下,测试脚本“语言”可以在相当高的级别上定义 - 足够高,甚至可以在系统构建之前手动编写测试脚本。事实上,据报道,Microsoft Excel 电子表格的 VBA 宏功能最初是作为 Excel 自动测试的机制。

It is also possible to build a Recorded Test capability into the SUT. In such a case, the test scripting "language" can be defined at a fairly high level—high enough to make it possible to hand-script the tests even before the system is built. In fact, it has been reported that the VBA macro capability of Microsoft's Excel spreadsheet started out as a mechanism for automated testing of Excel.

示例:内置测试记录

Example: Built-In Test Recording

从表面上看,为记录测试提供代码示例似乎没有什么意义,因为这种模式处理的是测试的生成方式,而不是测试的表示方式。回放测试时,它实际上是数据驱动测试(第288页)。同样,我们通常不会重构记录测试,因为这通常是项目中尝试的第一个测试自动化策略。尽管如此,如果我们发现有太多缺失测试(第 268页),我们可能会在尝试脚本测试后引入记录测试,因为手动自动化的成本太高。在这种情况下,我们不会尝试将现有的脚本测试转变为记录测试;我们只会记录新的测试。

On the surface, it doesn't seem to make sense to provide a code sample for a Recorded Test because this pattern deals with how the test is produced, not how it is represented. When the test is played back, it is in effect a Data-Driven Test (page 288). Likewise, we don't often refactor to a Recorded Test because it is often the first test automation strategy attempted on a project. Nevertheless, we might introduce a Recorded Test after attempting Scripted Tests if we discover that we have too many Missing Tests (page 268) because the cost of manual automation is too high. In that case, we would not be trying to turn existing Scripted Tests into Recorded Tests; we would just record new tests.

以下是应用程序本身记录的测试示例。此测试用于在将安全关键型应用程序从 OS2 上的 C 移植到 Windows 上的 C++ 后对其进行回归测试。请注意,记录的信息如何形成用户可读性极强的领域特定高级语言。

Here's an example of a test recorded by the application itself. This test was used to regression-test a safety-critical application after it was ported from C on OS2 to C++ on Windows. Note how the recorded information forms a domain-specific Higher-Level Language that is quite readable by a user.

<interaction-log>

      <commands>

            <!-- 省略了更多命令 -->

            <command seqno="2" id="Supply Create">

                  <field name="engineno" type="input">

                      <used-value>5566</used-value>

                      <expected></expected>

                      <actual status="ok"/>

                  </field>

                  <field name="direction" type="selection">

                      <used-value>SOUTH</used-value>

                      <expected>

                            <value>SOUTH</value>

                            <value>NORTH</value>

                      </expected>

                      <actual>

                          <value status="ok">SOUTH</value>

                          <value status="ok">NORTH</value>

                      </actual>

                  </field>

            </command>

            <!-- 省略了更多命令 -->

      </commands>

</interaction-log>

<interaction-log>

      <commands>

            <!--  more  commands  omitted  -->

            <command  seqno="2"  id="Supply  Create">

                  <field  name="engineno"  type="input">

                      <used-value>5566</used-value>

                      <expected></expected>

                      <actual  status="ok"/>

                  </field>

                  <field  name="direction"  type="selection">

                      <used-value>SOUTH</used-value>

                      <expected>

                            <value>SOUTH</value>

                            <value>NORTH</value>

                      </expected>

                      <actual>

                          <value  status="ok">SOUTH</value>

                          <value  status="ok">NORTH</value>

                      </actual>

                  </field>

            </command>

            <!--  more  commands  omitted  -->

      </commands>

</interaction-log>

 

此示例描述了回放测试的输出。actual元素由内置回放机制插入。status属性指示这些元素是否与expected值匹配。我们将样式表应用于这些文件,以将它们格式化为类似于 Fit 测试的格式,并使用颜色编码结果。然后,项目中的业务用户处理录制、回放和结果分析。

This sample depicts the output of having played back the tests. The actual elements were inserted by the built-in playback mechanism. The status attributes indicate whether these elements match the expected values. We applied a style sheet to these files to format them much like a Fit test with color-coded results. The business users on the project then handled the recording, replaying, and result analysis.

这种记录是通过在软件的表示层插入钩子来记录提供给用户的选项列表和用户的响应来实现的。其中一个钩子的示例如下:

This recording was made by inserting hooks in the presentation layer of the software to record the lists of choices offered the user and the user's responses. An example of one of these hooks follows:

如果 (playback_is_on()) {

    choice = get_choice_for_playback(dialog_id,choices_list);

} else {

    choice = display_dialog(choices_list,row,col,title,key);

}



如果 (recording_is_on()) {

    record_choice(dialog_id,choices_list,choice,key);

}

if  (playback_is_on())  {

    choice  =  get_choice_for_playback(dialog_id,  choices_list);

}  else  {

    choice    =  display_dialog(choices_list,  row,  col,  title,  key);

}



if  (recording_is_on())    {

    record_choice(dialog_id,  choices_list,  choice,  key);

}

 

该方法get_choice_for_playback检索元素的内容,used-value而不是要求用户从选项列表中进行选择。该方法record_choice生成actual元素并针对元素做出“断言” expected,将结果记录在status每个元素的属性中。请注意,只要我们处于回放模式,就会recording_is_on()返回true,以便记录测试结果。

The method get_choice_for_playback retrieves the contents of the used-value element instead of asking the user to pick from the list of choices. The method record_choice generates the actual element and makes the "assertions" against the expected elements, recording the result in the status attribute of each element. Note that recording_is_on() returns true whenever we are in playback mode so that the test results can be recorded.

示例:商业录制和播放测试工具

Example: Commercial Record and Playback Test Tool

几乎每个商业测试工具都使用“记录和回放”隐喻。每个工具还定义了自己的记录测试文件格式,其中大多数都非常冗长。以下是使用 Mercury Interactive 的 QuickTest Professional [ QTP ] 工具记录的测试的“简短”摘录。它显示在“专家视图”中,揭示了实际记录的内容:VbScript 程序!示例包括手动插入的注释(以“@@”开头),以阐明此测试正在做什么;如果在应用程序更改导致测试不再运行后重新记录测试,这些注释将会丢失。

Almost every commercial testing tool uses a "record and playback" metaphor. Each tool also defines its own Recorded Test file format, most of which are very verbose. The following is a "short" excerpt from a test recorded using Mercury Interactive's QuickTest Professional [QTP] tool. It is shown in "Expert View," which exposes what is really recorded: a VbScript program! The example includes comments (preceded by "@@") that were inserted manually to clarify what this test is doing; these comments would be lost if the test were rerecorded after a change to the application caused the test to no longer run.

@@

@@ GoToPageMaintainTaxonomy()

@@

Browser("Inf").Page("Inf").WebButton("Login").Click

Browser("Inf").Page("Inf_2").Check CheckPoint("Inf_2")

Browser("Inf").Page("Inf_2"").Link("TAXONOMY LINKING").Click

Browser("Inf").Page("Inf_3").Check CheckPoint("Inf_3")

Browser("Inf").Page("Inf_3").Link("MAINTAIN TAXONOMY").Click

Browser("Inf").Page("Inf_4").Check CheckPoint("Inf_4")

@@

@@ AddTerm("A","Top Level", "Top Level Definition")

@@

Browser("Inf").Page("Inf_4").Link("Add").Click

等待 4

Browser("Inf_2").Page("Inf").Check CheckPoint("Inf_5")

Browser("Inf_2").Page("Inf").WebEdit("childCodeSuffix").设置 "A"

Browser("Inf_2").Page("Inf").

     WebEdit("taxonomyDto.descript").设置 "顶层"

Browser("Inf_2").Page("Inf").

     WebEdit("taxonomyDto.definiti").设置 "顶层定义"

Browser("Inf_2").Page("Inf").WebButton("Save").单击

等待 4

Browser("Inf").Page("Inf_5").检查 CheckPoint("Inf_5_2")

@@

@@ SelectTerm("[A]-顶层")

@@

Browser("Inf").Page("Inf_5").

     WebList("selectedTaxonomyCode").选择 "[A]-顶层"

@@

@@ AddTerm("B","第二个顶层", "第二个顶层定义”)

@@

Browser(“Inf”)。Page(“Inf_5”)。Link(“Add”)。Click

等待 4

Browser(“Inf_2”)。Page(“Inf_2”)。Check CheckPoint(“Inf_2_2”)

      infofile_;_Inform_Alberta_21.inf_;_hightlight id_;

          _Browser(“Inf_2”)。Page(“Inf_2”)_;_

@@

@@ 并且它继续,继续,继续......

@@

@@  GoToPageMaintainTaxonomy()

@@

Browser("Inf").Page("Inf").WebButton("Login").Click

Browser("Inf").Page("Inf_2").Check  CheckPoint("Inf_2")

Browser("Inf").Page("Inf_2"").Link("TAXONOMY  LINKING").Click

Browser("Inf").Page("Inf_3").Check  CheckPoint("Inf_3")

Browser("Inf").Page("Inf_3").Link("MAINTAIN  TAXONOMY").Click

Browser("Inf").Page("Inf_4").Check  CheckPoint("Inf_4")

@@

@@  AddTerm("A","Top  Level",  "Top  Level  Definition")

@@

Browser("Inf").Page("Inf_4").Link("Add").Click

wait  4

Browser("Inf_2").Page("Inf").Check  CheckPoint("Inf_5")

Browser("Inf_2").Page("Inf").WebEdit("childCodeSuffix").Set  "A"

Browser("Inf_2").Page("Inf").

     WebEdit("taxonomyDto.descript").Set  "Top  Level"

Browser("Inf_2").Page("Inf").

     WebEdit("taxonomyDto.definiti").Set  "Top  Level  Definition"

Browser("Inf_2").Page("Inf").WebButton("Save").Click

wait  4

Browser("Inf").Page("Inf_5").Check  CheckPoint("Inf_5_2")

@@

@@  SelectTerm("[A]-Top  Level")

@@

Browser("Inf").Page("Inf_5").

     WebList("selectedTaxonomyCode").Select  "[A]-Top  Level"

@@

@@  AddTerm("B","Second  Top  Level",  "Second  Top  Level  Definition")

@@

Browser("Inf").Page("Inf_5").Link("Add").Click

wait  4

Browser("Inf_2").Page("Inf_2").Check  CheckPoint("Inf_2_2")

      infofile_;_Inform_Alberta_21.inf_;_hightlight  id_;

          _Browser("Inf_2").Page("Inf_2")_;_

@@

@@  and  it  goes  on,  and  on,  and  on  ....

 

请注意测试如何根据应用程序的用户界面描述所有输入和输出。它有两个主要问题:模糊测试(由记录信息的详细性质引起)和界面敏感性(导致脆弱测试)。

Note how the test describes all inputs and outputs in terms of the user interface of the application. It suffers from two main issues: Obscure Tests (caused by the detailed nature of the recorded information) and Interface Sensitivity (resulting in Fragile Tests).

重构说明

Refactoring Notes

我们可以使该测试作为文档更有用,减少或避免高​​测试维护成本第 265页),并通过使用一系列提取方法 [Fowler] 重构来支持从高级语言组合其他测试。

We can make this test more useful as documentation, reduce or avoid High Test Maintenance Cost (page 265), and support composition of other tests from a Higher-Level Language by using a series of Extract Method [Fowler] refactorings.

示例:重构商业录制测试

Example: Refactored Commercial Recorded Test

以下示例展示了重构为“传达意图”的相同测试(参见第41页):

The following example shows the same test refactored to Communicate Intent (see page 41):

GoToPage_MaintainTaxonomy()

AddTerm("A","顶级", "顶级定义")

SelectTerm("[A]-顶级")

AddTerm("B","第二顶级", "第二顶级定义")

GoToPage_MaintainTaxonomy()

AddTerm("A","Top  Level",  "Top  Level  Definition")

SelectTerm("[A]-Top  Level")

AddTerm("B","Second  Top  Level",  "Second  Top  Level  Definition")

 

注意这个测试揭示了多少意图。我们提取的测试实用程序方法如下所示:

Note how much more intent revealing this test has become. The Test Utility Methods we extracted look like this:

方法 GoToPage_MaintainTaxonomy()

      Browser("Inf").Page("Inf").WebButton("Login").Click

      Browser("Inf").Page("Inf_2").Check CheckPoint("Inf_2")

      Browser("Inf").Page("Inf_2").Link("TAXONOMY LINKING").Click

      Browser("Inf").Page("Inf_3").Check CheckPoint("Inf_3")

      Browser("Inf").Page("Inf_3").Link("MAINTAIN TAXONOMY").Click

      Browser("Inf").Page("Inf_4").Check CheckPoint("Inf_4")

结束



方法 AddTerm( 代码, 名称, 描述)

      Browser("Inf").Page("Inf_4").Link("Add").Click

      等待 4

      Browser("Inf_2").Page("Inf").Check CheckPoint("Inf_5")

      Browser("Inf_2").Page("Inf").

           WebEdit("childCodeSuffix").设置代码

      Browser("Inf_2").Page("Inf").

           WebEdit("taxonomyDto.descript").设置名称

      Browser("Inf_2").Page("Inf").

           WebEdit("taxonomyDto.definiti").设置描述

      Browser("Inf_2").Page("Inf").WebButton("Save").单击

      等待 4

      Browser("Inf").Page("Inf_5").检查 CheckPoint("Inf_5_2")

结束



方法 SelectTerm( 路径 )

      Browser("Inf").Page("Inf_5").

           WebList("selectedTaxonomyCode").选择路径

      Browser("Inf").Page("Inf_5").Link("Add").单击

      等待 4

结束

Method  GoToPage_MaintainTaxonomy()

      Browser("Inf").Page("Inf").WebButton("Login").Click

      Browser("Inf").Page("Inf_2").Check  CheckPoint("Inf_2")

      Browser("Inf").Page("Inf_2").Link("TAXONOMY  LINKING").Click

      Browser("Inf").Page("Inf_3").Check  CheckPoint("Inf_3")

      Browser("Inf").Page("Inf_3").Link("MAINTAIN  TAXONOMY").Click

      Browser("Inf").Page("Inf_4").Check  CheckPoint("Inf_4")

End



Method  AddTerm(  code,  name,  description)

      Browser("Inf").Page("Inf_4").Link("Add").Click

      wait  4

      Browser("Inf_2").Page("Inf").Check  CheckPoint("Inf_5")

      Browser("Inf_2").Page("Inf").

           WebEdit("childCodeSuffix").Set  code

      Browser("Inf_2").Page("Inf").

           WebEdit("taxonomyDto.descript").Set  name

      Browser("Inf_2").Page("Inf").

           WebEdit("taxonomyDto.definiti").Set  description

      Browser("Inf_2").Page("Inf").WebButton("Save").Click

      wait  4

      Browser("Inf").Page("Inf_5").Check  CheckPoint("Inf_5_2")

end



Method  SelectTerm(  path  )

      Browser("Inf").Page("Inf_5").

           WebList("selectedTaxonomyCode").Select  path

      Browser("Inf").Page("Inf_5").Link("Add").Click

      wait  4

end

 

这个示例是我拼凑起来的,用来说明与我们在 xUnit 中所做的相似之处。不要尝试在家里运行这个示例——它可能在语法上不正确。

This example is one I hacked together to illustrate the similarities to what we do in xUnit. Don't try running this example at home—it is probably not syntactically correct.

进一步阅读

论文“使用记录和回放进行敏捷回归测试” [ARTRP]描述了我们在应用程序中构建记录测试机制以便于将其移植到另一个平台的经验。

The paper "Agile Regression Testing Using Record and Playback" [ARTRP] describes our experiences building a Recorded Test mechanism into an application to facilitate porting it to another platform.

脚本测试

Scripted Test

也称为

Also known as

手写测试、手写脚本测试、程序化测试、自动化单元测试

Hand-Written Test, Hand-Scripted Test, Programmatic Test, Automated Unit Test

我们如何为我们的软件准备自动化测试?

How do we prepare automated tests for our software?

我们通过手动编写测试程序来实现测试的自动化。

We automate the tests by writing test programs by hand.

图像

自动化测试有多种用途。它们可用于软件更改后的回归测试。它们可以帮助记录软件的行为。它们可以在编写软件之前指定软件的行为。我们如何准备自动化测试脚本会影响它们的用途、它们对 SUT 更改的稳健性以及准备它们需要多少技能和精力。

Automated tests serve several purposes. They can be used for regression testing software after it has been changed. They can help document the behavior of the software. They can specify the behavior of the software before it has been written. How we prepare the automated test scripts affects which purpose they can be used for, how robust they are to changes in the SUT, and how much skill and effort it takes to prepare them.

脚本测试使我们能够在软件开发之前准备测试,以便它们可以帮助推动设计。

Scripted Tests allow us to prepare our tests before the software is developed so they can help drive the design.

工作原理

How It Works

我们通过编写与 SUT 交互的测试程序来自动化测试,以测试其功能。与录制的测试第 278页)不同,这些测试可以是客户测试或单元测试。这些测试程序通常称为“测试脚本”,以将其与测试的生产代码区分开来。

We automate our tests by writing test programs that interact with the SUT for the purpose of exercising its functionality. Unlike Recorded Tests (page 278), these tests can be either customer tests or unit tests. These test programs are often called "test scripts" to distinguish them from the production code they test.

何时使用它

When to Use It

在为我们的软件准备单元测试时,我们几乎总是使用脚本测试。这是因为从用相同编程语言编写的软件中直接访问单个单元更容易。它还允许我们练习所有代码路径,包括“病态”情况。

We almost always use Scripted Tests when preparing unit tests for our software. This is because it is easier to access the individual units directly from software written in the same programming language. It also allows us to exercise all the code paths, including the "pathological" cases.

客户测试的情况稍微复杂一些;每当我们使用自动化故事测试来推动软件开发时,都应该使用脚本测试。记录测试不能很好地满足这一需求,因为如果没有应用程序来记录测试,就很难记录测试。准备脚本测试需要编程经验和测试技术经验。项目中的大多数业务用户不太可能对学习如何准备脚本测试感兴趣。用编程语言编写测试脚本的另一种方法是定义一种高级语言(参见第41页)来测试 SUT,然后将该语言实现为数据驱动测试第 288页)解释器[GOF] 。用于定义数据驱动测试的开源框架是Fit及其基于 wiki 的兄弟FitNesse。Canoo WebTest是另一种支持这种测试风格的工具。

Customer tests are a slightly more complicated picture; we should use a Scripted Test whenever we use automated storytests to drive the development of software. Recorded Tests don't serve this need very well because it is difficult to record tests without having an application from which to record them. Preparing Scripted Tests takes programming experience as well as experience in testing techniques. It is unlikely that most business users on a project would be interested in learning how to prepare Scripted Tests. An alternative to scripting tests in a programming language is to define a Higher-Level Language (see page 41) for testing the SUT and then to implement the language as a Data-Driven Test (page 288) Interpreter [GOF]. An open-source framework for defining Data-Driven Tests is Fit and its wiki-based cousin, FitNesse. Canoo WebTest is another tool that supports this style of testing.

对于现有的遗留应用程序3 ,我们可以考虑使用录制测试来快速创建一套回归测试,以便在我们重构代码以引入可测试性时保护我们。然后,我们可以为现在可测试的应用程序准备脚本测试。

In case of an existing legacy application,3 we can consider using Recorded Tests as a way of quickly creating a suite of regression tests that will protect us while we refactor the code to introduce testability. We can then prepare Scripted Tests for our now testable application.

实施说明

Implementation Notes

传统上,脚本测试被编写为“测试程序”,通常使用特殊的测试脚本语言。如今,我们更喜欢使用测试自动化框架(第 298页)(例如 xUnit)和与 SUT 相同的语言来编写脚本测试。在这种情况下,每个测试程序通常以测试方法(第 348页)的形式捕获,这些方法位于测试用例类(第 373页) 上。为了最大限度地减少人工干预(第 250页),每个测试方法都应实现自检测试(参见第26页),该测试也是可重复测试(参见第26页)。

Traditionally, Scripted Tests were written as "test programs," often using a special test scripting language. Nowadays, we prefer to write Scripted Tests using a Test Automation Framework (page 298) such as xUnit in the same language as the SUT. In this case, each test program is typically captured in the form of a Test Method (page 348) on a Testcase Class (page 373). To minimize Manual Intervention (page 250), each test method should implement a Self-Checking Test (see page 26) that is also a Repeatable Test (see page 26).

示例:脚本测试

Example: Scripted Test

以下是用 JUnit 编写的脚本测试的示例:

The following is an example of a Scripted Tests written in JUnit:

public void testAddLineItem_quantityOne(){

      final BigDecimal BASE_PRICE = UNIT_PRICE;

      final BigDecimal EXTENDED_PRICE = BASE_PRICE;

      // 设置固定装置

      Customer customer = createACustomer(NO_CUST_DISCOUNT);

      Invoice invoice = createInvoice(customer);

      // 练习 SUT

      invoice.addItemQuantity(PRODUCT, QUAN_ONE);

      // 验证结果

      LineItem expected =

          createLineItem( QUAN_ONE, NO_CUST_DISCOUNT,

                                     EXTENDED_PRICE, PRODUCT, invoice);

      assertContainsExactlyOneLineItem( invoice, expected );

}



public void testChangeQuantity_severalQuantity(){

      final int ORIGINAL_QUANTITY = 3;

      final int NEW_QUANTITY = 5;

      final BigDecimal BASE_PRICE =

              UNIT_PRICE.multiply( new BigDecimal(NEW_QUANTITY));

      final BigDecimal EXTENDED_PRICE =

              BASE_PRICE.subtract(BASE_PRICE.multiply(

                                      CUST_DISCOUNT_PC.movePointLeft(2)));

      // 设置固定装置

      客户客户 = createACustomer(CUST_DISCOUNT_PC);

      发票发票 = createInvoice(customer);

      产品产品 = createAProduct( UNIT_PRICE);

      发票.addItemQuantity(product, ORIGINAL_QUANTITY);

      // 练习 SUT

      发票.changeQuantityForProduct(product, NEW_QUANTITY);

      // 验证结果

      LineItem expected = createLineItem( NEW_QUANTITY,

          CUST_DISCOUNT_PC, EXTENDED_PRICE, PRODUCT, 发票);

      assertContainsExactlyOneLineItem( 发票, 预期 );

}

public  void  testAddLineItem_quantityOne(){

      final  BigDecimal  BASE_PRICE  =  UNIT_PRICE;

      final  BigDecimal  EXTENDED_PRICE  =  BASE_PRICE;

      //    Set  Up  Fixture

      Customer  customer  =  createACustomer(NO_CUST_DISCOUNT);

      Invoice  invoice  =  createInvoice(customer);

      //     Exercise  SUT

      invoice.addItemQuantity(PRODUCT,  QUAN_ONE);

      //  Verify  Outcome

      LineItem  expected  =

          createLineItem(  QUAN_ONE,  NO_CUST_DISCOUNT,

                                     EXTENDED_PRICE,  PRODUCT,  invoice);

      assertContainsExactlyOneLineItem(  invoice,  expected  );

}



public  void  testChangeQuantity_severalQuantity(){

      final  int  ORIGINAL_QUANTITY  =  3;

      final  int  NEW_QUANTITY  =  5;

      final  BigDecimal  BASE_PRICE  =

              UNIT_PRICE.multiply(      new  BigDecimal(NEW_QUANTITY));

      final  BigDecimal  EXTENDED_PRICE  =

              BASE_PRICE.subtract(BASE_PRICE.multiply(

                                      CUST_DISCOUNT_PC.movePointLeft(2)));

      //          Set  Up  Fixture

      Customer  customer  =  createACustomer(CUST_DISCOUNT_PC);

      Invoice  invoice  =  createInvoice(customer);

      Product  product  =  createAProduct(  UNIT_PRICE);

      invoice.addItemQuantity(product,  ORIGINAL_QUANTITY);

      //  Exercise  SUT

      invoice.changeQuantityForProduct(product,  NEW_QUANTITY);

      //  Verify  Outcome

      LineItem  expected  =  createLineItem(  NEW_QUANTITY,

          CUST_DISCOUNT_PC,  EXTENDED_PRICE,  PRODUCT,  invoice);

      assertContainsExactlyOneLineItem(  invoice,  expected  );

}

 

关于名称

About the Name

自动化测试程序传统上被称为“测试脚本”,这可能是由于此类测试程序的传统——最初它们是用 Tcl 等解释型测试脚本语言实现的。将它们称为脚本测试的缺点是,这种命名法容易让人混淆,它与人们在手动测试期间遵循的脚本类型以及探索性测试等非脚本测试相混淆。

Automated test programs are traditionally called "test scripts," probably due to the heritage of such test programs—originally they were implemented in interpreted test scripting languages such as Tcl. The downside of calling them Scripted Tests is that this nomenclature opens the door to confusion with the kind of script a person would follow during manual testing as opposed to unscripted testing such as exploratory testing.

进一步阅读

很多书籍都介绍了编写脚本测试的过程以及如何使用它们来推动 SUT 的设计。[TDD-BE] 或 [TDD-APG] 是一个不错的起点。

Many books have been written about the process of writing Scripted Tests and using them to drive the design of the SUT. A good place to start would be [TDD-BE] or [TDD-APG].

数据驱动测试

Data-Driven Test

我们如何为软件准备自动化测试?

如何减少测试代码重复?

How do we prepare automated tests for our software?

How do we reduce Test Code Duplication?

我们将每个测试所需的所有信息存储在数据文件中,并编写一个读取该文件并执行测试的解释器。

We store all the information needed for each test in a data file and write an interpreter that reads the file and executes the tests.

图像

测试可能非常重复,不仅因为我们必须一遍又一遍地运行相同的测试,还因为许多测试只有细微的差别。例如,我们可能希望运行本质上相同的测试,但系统输入略有不同,并验证实际输出是否相应地变化。这些测试中的每一个都由完全相同的步骤组成。虽然进行如此多的测试是确保良好功能覆盖率的绝佳方式,但这对于测试的可维护性来说并不好,因为对其中一个测试的算法所做的任何更改都必须传播到所有类似的测试。

Testing can be very repetitious not only because we must run the same test over and over again, but also because many of the tests differ only slightly. For example, we might want to run essentially the same test with slightly different system inputs and verify that the actual output varies accordingly. Each of these tests would consist of exactly the same steps. While having so many tests is an excellent way to ensure good coverage of functionality, it is not so good for test maintainability because any change made to the algorithm of one of these tests must be propagated to all of the similar tests.

数据驱动测试是一种获得出色覆盖率的方法,同时最大限度地减少我们需要编写和维护的测试代码量。

A Data-Driven Test is one way to get excellent coverage while minimizing the amount of test code we need to write and maintain.

工作原理

How It Works

我们编写了一个数据驱动测试解释器,其中包含测试中的所有通用逻辑。我们将不同测试中不同的数据放入数据驱动测试文件中,解释器将读取该文件来执行测试。对于每个测试,它执行相同的操作序列来实现四阶段测试(第358页)。首先,解释器从文件中检索测试数据,并使用文件中的数据设置测试装置。其次,它使用文件指定的任何参数来执行 SUT。第三,它将 SUT 产生的实际结果(例如,返回值、测试后状态)与文件中的预期结果进行比较。如果结果不匹配,它将测试标记为失败;如果 SUT 抛出异常,它会捕获异常并相应地标记测试并继续。第四,解释器执行任何必要的装置拆卸,然后转到文件中的下一个测试。

We write a Data-Driven Test interpreter that contains all the common logic from the tests. We put the data that varies from test to test into the Data-Driven Test file that the interpreter reads to execute the tests. For each test it performs the same sequence of actions to implement the Four-Phase Test (page 358). First, the interpreter retrieves the test data from the file and sets up the test fixture using the data from the file. Second, it exercises the SUT with whatever arguments the file specifies. Third, it compares the actual results produced by the SUT (e.g., returned values, post-test state) with the expected results from the file. If the results don't match, it marks the test as failed; if the SUT throws an exception, it catches the exception and marks the test accordingly and continues. Fourth, the interpreter does any fixture teardown that is necessary and then moves on to the next test in the file.

原本需要一系列复杂步骤的测试可以简化为数据驱动测试文件中的一行数据。Fit 是编写数据驱动测试的框架的一个流行示例。

A test that might otherwise require a series of complex steps can be reduced to a single line of data in the Data-Driven Test file. Fit is a popular example of a framework for writing Data-Driven Tests.

何时使用它

When to Use It

数据驱动测试是记录测试(第 278页) 和脚本测试(第 285页) 的替代策略。但它也可以用作脚本测试策略的一部分,并且记录测试在回放时实际上是数据驱动测试。数据驱动测试是让业务人员参与编写自动化测试的理想策略。通过保持数据文件格式简单,我们使业务人员能够用数据填充文件并执行测试,而无需让技术人员为每个测试编写测试代码。

A Data-Driven Test is an alternative strategy to a Recorded Test (page 278) and a Scripted Test (page 285). It can also be used as part of a Scripted Test strategy, however, and Recorded Tests are, in fact, Data-Driven Tests when they are played back. A Data-Driven Test is an ideal strategy for getting business people involved in writing automated tests. By keeping the format of the data file simple, we make it possible for the business person to populate the file with data and execute the tests without having to ask a technical person to write test code for each test.

每当我们有大量不同的数据值并希望使用 SUT 来执行测试时,我们可以考虑将数据驱动测试用作脚本测试策略的一部分,其中每个数据值都必须执行相同的步骤序列。通常,我们会随着时间的推移发现这种相似性,并首先重构为参数化测试第 607页),然后再重构为数据驱动测试。我们可能还希望按照不同的顺序安排一组标准步骤,并使用不同的数据值,就像在增量表格测试中一样(请参阅参数化测试)。这种方法让我们能够以最少的测试代码维护获得最佳覆盖率,并且可以根据需要轻松添加更多测试。

We can consider using a Data-Driven Test as part of a Scripted Test strategy whenever we have a lot of different data values with which we wish to exercise the SUT where the same sequence of steps must be executed for each data value. Usually, we discover this similarity over time and refactor first to a Parameterized Test (page 607) and then to a Data-Driven Test. We may also want to arrange a standard set of steps in different sequences with different data values much like in an Incremental Tabular Test (see Parameterized Test). This approach gives us the best coverage with the least amount of test code to maintain and makes it very easy to add more tests as they are needed.

在决定是否使用数据驱动测试时,另一个考虑因素是测试的行为是硬编码的还是由配置数据驱动的。如果我们使用脚本测试自动执行数据驱动行为的测试,则每当配置数据发生变化时,我们都必须更新测试程序。这种行为完全不自然,因为这意味着每当我们更改配置数据库中的数据时,都必须将更改提交到源代码存储库 [SCM]。4通过使测试成为数据驱动,对配置数据或元对象的更改将由对数据驱动测试的更改驱动- 这是一种更自然的关系。

Another consideration when deciding whether to use Data-Driven Tests is whether the behavior we are testing is hard-coded or driven by configuration data. If we automate tests for data-driven behavior using Scripted Tests, we must update the test programs whenever the configuration data changes. This behavior is just plain unnatural because it implies that we must commit changes to our source code repository [SCM] whenever we change the data in our configuration database.4 By making the tests data-driven, changes to the configuration data or meta-objects are then driven by changes to the Data-Driven Tests—a much more natural relationship.

实施说明

Implementation Notes

我们的实施选项取决于我们是将数据驱动测试用作一种独特的测试策略,还是将其作为基于 xUnit 的策略的一部分。将数据驱动测试用作独立测试策略通常涉及使用开源工具(例如 Fit)或商业记录测试工具(例如 QTP)。将数据驱动测试用作脚本测试策略的一部分可能涉及在 xUnit 中实施数据驱动测试解释器。

Our implementation options depend on whether we are using a Data-Driven Test as a distinct test strategy or as part of an xUnit-based strategy. Using a Data-Driven Test as a stand-alone test strategy typically involves using open-source tools such as Fit or commercial Recorded Test tools such as QTP. Using a Data-Driven Test as part of a Scripted Test strategy may involve implementing a Data-Driven Test interpreter within xUnit.

无论我们选择采用哪种策略,我们都应该使用适当的测试自动化框架第 298页)(如果有)。通过这样做,我们可以有效地将测试转换为两个部分:数据驱动测试解释器和数据驱动测试文件。这两项资产都应置于版本控制之下,以便我们可以看到它们随着时间的推移如何发展,并允许我们撤消任何误导性更改。将数据驱动测试文件存储在某种存储库中尤为重要,即使这个概念对业务用户来说可能很陌生。我们可以通过为用户提供数据驱动测试文件创作工具(如 FitNesse)来使此操作透明化,或者我们可以设置一个“用户友好”的存储库(如恰好也支持版本控制的文档管理系统)。

Regardless of which strategy we elect to follow, we should use the appropriate Test Automation Framework (page 298) if one is available. By doing so, we effectively convert our tests into two parts: the Data-Driven Test interpreter and the Data-Driven Test files. Both of these assets should be kept under version control so that we can see how they have evolved over time and to allow us to back out any misguided changes. It is particularly important to store the Data-Driven Test files in some kind of Repository, even though this concept may be foreign to business users. We can make this operation transparent by providing the users with a Data-Driven Test file-authoring tool such as FitNesse, or we can set up a "user-friendly" Repository such as a document management system that just happens to support version control as well.

作为持续集成过程的一部分,运行这些测试也很重要,以确保曾经通过的测试不会突然失败。如果不这样做,软件中就会潜入未被发现的错误,一旦检测到错误,就需要进行大量的故障排除工作。将客户测试纳入持续集成过程需要某种方式来跟踪哪些客户测试已经通过,因为我们并不坚持在提交任何代码之前所有客户测试都通过。一种选择是保留两组输入文件,将通过的测试从“仍为红色”文件迁移到“全部为绿色”文件,该文件用于自动构建过程中的回归测试。

It is also important to run these tests as part of the continuous integration process to confirm that tests that once passed do not suddenly begin to fail. Failing to do so can result in bugs creeping into the software undetected and significant troubleshooting effort once the bugs are detected. Including the customer tests in the continuous integration process requires some way to keep track of which customer tests were already passing, because we don't insist that all customer tests pass before any code is committed. One option is to keep two sets of input files, migrating tests that pass from the "still red" file into the "all green" file that is used for regression testing as part of the automatic build process.

变体:数据驱动测试框架(Fit)

当我们使用数据驱动测试作为测试策略时,我们应该考虑使用预构建的数据驱动测试框架。Fit 最初是由 Ward Cunningham 构想的框架,旨在让业务用户参与测试自动化。尽管 Fit 通常用于自动化客户测试,但如果测试数量足以构建必要的装置,它也可以用于单元测试。Fit 由两部分组成:框架和用户创建的装置。Fit 框架是一个通用的数据驱动测试解释器,它读取输入文件并查找其中的所有表。它在每个表的左上角单元格中查找装置类名,然后在我们的测试可执行文件中搜索该类。当它找到一个类时,它会创建该类的一个实例,并在读取表的每一行和每一列时将控制权传递给该实例。我们可以覆盖框架定义的方法来指定表中每个单元格应该发生什么。然后,Fit 装置是一个适配器,Fit 调用它来解释数据表并调用 SUT 上的方法。

We should consider using a prebuilt Data-Driven Test framework when we are using Data-Driven Tests as a test strategy. Fit is a framework originally conceived by Ward Cunningham as a way of involving business users in the automation of tests. Although Fit is typically used to automate customer tests, it can also be used for unit tests if the number of tests warrants building the necessary fixtures. Fit consists of two parts: the framework and a user-created fixture. The Fit Framework is a generic Data-Driven Test interpreter that reads the input file and finds all tables in it. It looks in the top-left cell of each table for a fixture classname and then searches our test executable for that class. When it finds a class, it creates an instance of the class and passes control to that instance as it reads each row and column of the table. We can override methods defined by the framework to specify what should happen for each cell in the table. A Fit fixture, then, is an adapter that Fit calls to interpret a table of data and invoke methods on the SUT.

Fit 表还可以包含 SUT 的预期结果。Fit 将指定的值与 SUT 返回的实际值进行比较。但是,与 xUnit 中的断言方法(第 362页) 不同,Fit 不会在第一个与预期值不匹配的值时放弃测试。相反,它会在表中的每个单元格中着色,绿色单元格表示与预期值匹配的实际值,红色单元格表示错误或意外的值。

The Fit table can also contain expected results from the SUT. Fit compares the specified values with the actual values returned by the SUT. Unlike Assertion Methods (page 362) in xUnit, however, Fit does not abandon a test at the first value that does not match the expected value. Instead, it colors in each cell in the table, with green cells indicating actual values that matched the expected values and red cells indicating wrong or unexpected values.

使用 Fit 有几个优点:

Using Fit offers several advantages:

  • 与我们构建自己的测试解释器[GOF]相比,需要编写的代码要少得多。
  • There is much less code to write than when we build our own test Interpreter [GOF].
  • 该输出对于商务人士来说是有意义的,而不仅仅是对于技术人员来说。
  • The output makes sense to a business person, not just a technical person.
  • 测试不会在第一次失败的断言时停止。Fit 有一种传达多个故障/错误的方法,可以让我们非常轻松地看到故障模式。
  • The tests don't stop at the first failed assertion. Fit has a way of communicating multiple failures/errors in a way that allows us to see the failure patterns very easily.
  • 有大量的装置类型可供子类化或按原样使用。
  • There are a plethora of fixture types available to subclass or use as is.

那么,为什么我们不使用 Fit 而不是 xUnit 来进行所有单元测试呢?使用 Fit 的主要缺点如下所述:

So why wouldn't we use Fit for all our unit testing instead of xUnit? The main disadvantages of using Fit are described here:

  • 在构建 Fit 装置之前,我们需要非常了解测试场景。然后,我们需要将每个测试的逻辑转换为表格形式;这并不总是合适的,尤其是对于习惯于程序化思考的开发人员而言。虽然让测试人员为客户测试编写 Fit 装置可能是合适的,但除非我们的测试人员与开发人员的比例接近 1:1,否则这种方法不适合真正的单元测试。
  • The test scenarios need to be very well understood before we can build the Fit fixture. We then need to translate each test's logic into a tabular representation; this isn't always a good fit, especially for developers who are used to thinking procedurally. While it may be appropriate to have testers who can write the Fit fixtures for customer tests, this approach wouldn't be appropriate for true unit tests unless we had close to a 1:1 tester-to-developer ratio.
  • 测试需要在每个测试中使用相同的 SUT 交互逻辑。5运行几种不同类型的测试,我们可能必须为每种类型的测试构建一个或多个不同的装置。构建新装置通常比编写一些测试方法(第 348页) 更复杂。
  • The tests need to employ the same SUT interaction logic in each test.5 To run several different styles of tests, we would probably have to build one or more different fixtures for each style of test. Building a new fixture is typically more complex than writing a few Test Methods (page 348).

尽管有许多不同类型的装置可供子类化或按原样使用,但开发人员需要学习如何使用它们才能完成工作。即便如此,并非所有单元测试都适合使用 Fit 进行自动化。

Although many different fixture types are available to subclass or use as is, their use in this way is yet another thing that developers would be required to learn to do their jobs. Even then, not all unit tests are amenable to automation using Fit.

  • Fit 测试通常不会集成到通过 xUnit 运行的开发人员回归测试中。相反,这些测试必须单独运行 — 这就引入了它们不会在每次签入时运行的可能性。一些团队将 Fit 测试作为其持续集成构建过程的一部分,以部分缓解此问题。其他团队报告称,拥有第二个“客户”构建服务或运行所有客户测试的服务器取得了巨大成功。
  • Fit tests aren't normally integrated into developers' regression tests that are run via xUnit. Instead, these tests must be run separately—which introduces the possibility that they will not be run at each check-in. Some teams include Fit tests as part of their continuous integration build process to partially mitigate this issue. Other teams have reported great success having a second "customer" build service or server that runs all the customer tests.

当然,这些问题都是可以克服的。一般来说,xUnit 比 Fit 更适合单元测试;反之亦然。

Each of these issues is potentially surmountable, of course. In general, xUnit is a more appropriate framework for unit testing than Fit; the reverse is true for customer tests.

变体:简单的 xUnit 测试解释器

当我们有少量数据驱动测试希望作为基于 xUnit 的脚本测试策略的一部分来运行时,最简单的实现是编写一个测试方法,其中包含一个循环,该循环从文件中读取一组输入数据值以及预期结果。这相当于将单个参数化测试及其所有调用者转换为表格测试(请参阅参数化测试)。与表格测试一样,这种构建数据驱动测试解释器的方法将产生一个带有许多断言的单个测试用例对象第 382页)。这有几个后果:

When we have a small number of Data-Driven Tests that we wish to run as part of an xUnit-based Scripted Test strategy, the simplest implementation is to write a Test Method containing a loop that reads one set of input data values from the file along with the expected results. This is the equivalent of converting a single Parameterized Test and all its callers into a Tabular Test (see Parameterized Test). As with a Tabular Test, this approach to building the Data-Driven Test interpreter will result in a single Testcase Object (page 382) with many assertions. This has several ramifications:

  • 整个数据驱动测试集将计为单个测试。因此,将一组参数化测试转换为单个数据驱动测试将减少执行的测试数量。
  • The entire set of Data-Driven Tests will count as a single test. Hence, converting a set of Parameterized Tests into a single Data-Driven Test will reduce the count of tests executed.
  • 第一次失败或错误时,我们将停止执行数据驱动测试。结果,我们将失去很多缺陷定位(参见第22页)。xUnit 的一些变体确实允许我们指定失败的断言不应中止测试方法的执行。
  • We will stop executing the Data-Driven Test on the first failure or error. As a consequence, we will lose a lot of our Defect Localization (see page 22). Some variants of xUnit do allow us to specify that failed assertions shouldn't abort execution of the Test Method.
  • 我们需要确保断言失败告诉我们失败发生时我们正在执行哪个子测试。
  • We need to make sure our assertion failures tell us which subtest we were executing when the failure occurred.

我们可以通过在循环内包含一个语句但包围测试逻辑,然后继续执行代码来解决最后两个问题try/catch。尽管如此,我们仍然需要找到一种以有意义的方式报告测试结果的方法(例如,“子测试 1、3 和 6 失败,其中 ......”)。

We could address the last two issues by including a try/catch statement inside the loop but surrounding the test logic and then continuing the code's execution. Nevertheless, we still need to find a way to report the test results in a meaningful way (e.g., "Failed subtests 1, 3, and 6 with . . .").

为了更轻松地扩展数据驱动测试解释器以处理同一数据文件中的几种不同类型的测试,我们可以将“动词”或“动作词”作为数据文件的每个条目的一部分。然后,解释器可以根据动作词分派到不同的参数化测试。

To make it easier to extend the Data-Driven Test interpreter to handle several different kinds of tests in the same data file, we can include a "verb" or "action word" as part of each entry in the data file. The interpreter can then dispatch to a different Parameterized Test based on the action word.

变体:测试套件对象生成器

我们可以避免与简单 xUnit 测试解释器相关的“第一次失败就停止”问题,方法是让测试套件工厂(请参阅第 399页的测试枚举)suite上的方法构造与测试发现(第 393页) 的内置机制相同的测试套件对象(第 387页) 结构。为此,我们为数据驱动测试文件中的每个条目构建一个测试用例对象,并使用特定测试的测试数据初始化每个对象。6对象知道如何使用构建测试套件时加载到其中的数据来执行参数化测试。这确保了即使在第一个测试用例对象遇到断言失败后,数据驱动测试仍会继续执行。然后,我们可以让测试运行器(第 377页) 以正常方式计数测试、错误和失败。

We can avoid the "stop on first failure" problem associated with a Naive xUnit Test Interpreter by having the suite method on the Test Suite Factory (see Test Enumeration on page 399) fabricate the same Test Suite Object (page 387) structure as the built-in mechanism for Test Discovery (page 393). To do so, we build a Testcase Object for each entry in the Data-Driven Test file and initialize each object with the test data for the particular test.6 That object knows how to execute the Parameterized Test with the data loaded into it when the test suite was built. This ensures that the Data-Driven Test continues executing even after the first Testcase Object encounters an assertion failure. We can then let the Test Runner (page 377) count the tests, errors, and failures in the normal way.

变体:测试套件对象模拟器

构建测试套件对象的另一种方法是创建一个行为类似于测试用例的对象。此对象读取数据驱动测试文件并在要求运行时遍历所有测试。它必须捕获参数化测试抛出的任何异常并继续执行后续测试。完成后,测试用例对象必须向测试运行器报告正确的测试、失败和错误数量。它还需要实现测试运行器所依赖的标准测试接口上的任何其他方法,例如返回“套件”中的测试数量、返回套件中每个测试的名称和状态(对于图形测试树资源管理器,请参阅测试运行器),等等。

An alternative to building the Test Suite Object is to create a Testcase Object that behaves like one. This object reads the Data-Driven Test file and iterates over all the tests when asked to run. It must catch any exceptions thrown by the Parameterized Test and continue executing the subsequent tests. When finished, the Testcase Object must report the correct number of tests, failures, and errors back to the Test Runner. It also needs to implement any other methods on the standard test interface on which the Test Runner depends, such as returning the number of tests in the "suite," returning the name and status of each test in the suite (for the Graphical Test Tree Explorer, see Test Runner), and so forth.

激励人心的例子

Motivating Example

假设我们有一组如下的测试:

Let's assume we have a set of tests as follows:

def test_extref

        sourceXml = "<extref id='abc' />"

        expectedHtml = "<a href='abc.html'>abc</a>"

        generateAndVerifyHtml(sourceXml,expectedHtml,"<extref>")

end



def test_testterm_normal

    sourceXml = "<testterm id='abc'/>"

    expectedHtml = "<a href='abc.html'>abc</a>"

    generateAndVerifyHtml(sourceXml,expectedHtml,"<testterm>")

end



def test_testterm_plural

    sourceXml = "<testterms id='abc'/>"

    expectedHtml = "<a href='abc.html'>abcs</a>"

    generateAndVerifyHtml(sourceXml,expectedHtml,"<plural>")

end

def  test_extref

        sourceXml  =  "<extref  id='abc'  />"

        expectedHtml  =  "<a  href='abc.html'>abc</a>"

        generateAndVerifyHtml(sourceXml,expectedHtml,"<extref>")

end



def  test_testterm_normal

    sourceXml  =  "<testterm  id='abc'/>"

    expectedHtml  =  "<a  href='abc.html'>abc</a>"

    generateAndVerifyHtml(sourceXml,expectedHtml,"<testterm>")

end



def  test_testterm_plural

    sourceXml  =  "<testterms  id='abc'/>"

    expectedHtml  =  "<a  href='abc.html'>abcs</a>"

    generateAndVerifyHtml(sourceXml,expectedHtml,"<plural>")

end

 

通过定义参数化测试如下,可以实现这些测试的简洁性:

The succinctness of these tests is made possible by defining the Parameterized Test as follows:

def generateAndVerifyHtml( sourceXml, expectedHtml,

                                      message, &block)

    mockFile = MockFile.new

    sourceXml.delete!("\t")

    @handler = setupHandler(sourceXml, mockFile )

    block.call 除非 block == nil

    @handler.printBodyContents

    actual_html = mockFile.output

    assert_equal_html( expectedHtml,

                                  actual_html,

                                  message + "html output")

      actual_html

end

def  generateAndVerifyHtml(  sourceXml,  expectedHtml,

                                      message,  &block)

    mockFile  =  MockFile.new

    sourceXml.delete!("\t")

    @handler  =  setupHandler(sourceXml,  mockFile  )

    block.call  unless  block  ==  nil

    @handler.printBodyContents

    actual_html  =  mockFile.output

    assert_equal_html(  expectedHtml,

                                  actual_html,

                                  message  +  "html  output")

      actual_html

end

 

这些测试的主要问题是它们仍然是用代码编写的,而事实上,它们之间唯一的区别是用作输入的数据。

The main problem with these tests is that they are still written in code when, in fact, the only difference between them is the data used as input.

重构说明

Refactoring Notes

当然,解决方案是将参数化测试的通用逻辑提取到数据驱动测试解释器中,并将所有参数集收集到任何人都可以编辑的单个数据文件中。我们需要编写一个“主”测试,它知道从哪个文件读取测试数据,并编写一些逻辑来读取和解析测试文件。此逻辑可以调用我们现有的参数化测试逻辑,并让 xUnit 为我们跟踪测试执行统计信息。

The solution, of course, is to extract the common logic of the Parameterized Tests into a Data-Driven Test interpreter and to collect all sets of parameters into a single data file that can be edited by anyone. We need to write a "main" test that knows which file to read the test data from and a bit of logic to read and parse the test file. This logic can call our existing Parameterized Test logic and let xUnit keep track of the test execution statistics for us.

示例:使用 XML 数据文件的 xUnit 数据驱动测试

Example: xUnit Data-Driven Test with XML Data File

在此示例中,我们将使用 XML 作为文件表示。每个测试由一个test元素组成,该元素包含三个主要部分:

In this example, we will use XML as our file representation. Each test consists of a test element with three main parts:

  • 告诉数据驱动测试解释器运行哪个测试逻辑的操作(例如crossref
  • An action that tells the Data-Driven Test interpreter which test logic to run (e.g., crossref)
  • 传递给 SUT 的输入——在​​本例中为sourceXml元素
  • The input to be passed to the SUT—in this case, the sourceXml element
  • 我们期望 SUT 生成的 HTML(在expectedHtml元素中)
  • The HTML we expect the SUT to produce (in the expectedHtml element)

这三个组件被包裹在一个testsuite元素中。

These three components are wrapped up in a testsuite element.

<testsuite id="CrossRefHandlerTest">

    <test id="extref">

        <action>crossref</action>

        <sourceXml>

              <extref id='abc'/>

        </sourceXml>

        <expectedHtml>

              <a href='abc.html'>abc</a>

        </expectedHtml>

    </test>

    <test id="TestTerm">

        <action>crossref</action>

        <sourceXml>

              <testterm id='abc'/>

        </sourceXml>

        <expectedHtml>

              <a href='abc.html'>abc</a>

        </expectedHtml>

    </test>

    <test id="TestTerm Plural">

        <action>crossref</action>

        <sourceXml>

              <testterms id='abc'/>

        </sourceXml>

        <expectedHtml>

              <a href='abc.html'>abcs</a>

        </expectedHtml>

    </test>

测试套件

<testsuite  id="CrossRefHandlerTest">

    <test  id="extref">

        <action>crossref</action>

        <sourceXml>

              <extref  id='abc'/>

        </sourceXml>

        <expectedHtml>

              <a  href='abc.html'>abc</a>

        </expectedHtml>

    </test>

    <test  id="TestTerm">

        <action>crossref</action>

        <sourceXml>

              <testterm  id='abc'/>

        </sourceXml>

        <expectedHtml>

              <a  href='abc.html'>abc</a>

        </expectedHtml>

    </test>

    <test  id="TestTerm  Plural">

        <action>crossref</action>

        <sourceXml>

              <testterms  id='abc'/>

        </sourceXml>

        <expectedHtml>

              <a  href='abc.html'>abcs</a>

        </expectedHtml>

    </test>

</testsuite>

 

任何拥有 XML 编辑器的人都可以编辑此 XML 文件,无需担心引入测试逻辑错误。验证预期结果的所有逻辑都由数据驱动测试解释器封装,其封装方式与参数化测试非常相似。为了便于查看,我们可以通过定义样式表来隐藏 XML 结构。此外,许多 XML 编辑器会将 XML 转换为基于表单的输入,以简化编辑。

This XML file could be edited by anyone with an XML editor without any concern for introducing test logic errors. All the logic for verifying the expected outcome is encapsulated by the Data-Driven Test interpreter in much the same way as it would be by a Parameterized Test. For viewing purposes we could hide the XML structure from the user by defining a style sheet. In addition, many XML editors will turn the XML into a form-based input to simplify editing.

为了避免处理操作 XML 的复杂性,解释器还可以使用 CSV 文件作为输入。

To avoid dealing with the complexities of manipulating XML, the interpreter can also use a CSV file as input.

示例:使用 CSV 输入文件的 xUnit 数据驱动测试

Example: xUnit Data-Driven Test with CSV Input File

上例中的测试在 CSV 文件中看起来如下:

The test in the previous example would look like this as a CSV file:

ID、操作、SourceXml、ExpectedHtml

Extref、crossref、<extref id='abc'/>、<a href='abc.html'>abc</a>

TTerm、crossref、<testterm id='abc'/>、<a href='abc.html'>abc</a>

TTerms、crossref、<testterms id='abc'/>、<a href='abc.html'>abcs</a>

ID,    Action,         SourceXml,           ExpectedHtml

Extref,crossref,<extref  id='abc'/>,<a  href='abc.html'>abc</a>

TTerm,crossref,<testterm  id='abc'/>,<a  href='abc.html'>abc</a>

TTerms,crossref,<testterms  id='abc'/>,<a  href='abc.html'>abcs</a>

 

解释器相对简单,基于我们已经为参数化测试开发的逻辑构建。此版本读取 CSV 文件并使用 Ruby 的split函数解析每一行。

The interpreter is relatively simple and is built on the logic we had already developed for our Parameterized Test. This version reads the CSV file and uses Ruby's split function to parse each line.

def test_crossref

    executeDataDrivenTest "CrossrefHandlerTest.txt"

end



def executeDataDrivenTest filename

    dataFile = File.open(filename)

    dataFile.each_line do | line |

        desc, action, part2 = line.split(",")

          sourceXml, expectedHtml, leftOver = part2.split(",")

            if "crossref"==action.strip

              generateAndVerifyHtml sourceXml, expectedHtml, desc

        else # 新的“动词”作为 elsif 的

              report_error( "unknown action" + action.strip )

        end

    end

end

def  test_crossref

    executeDataDrivenTest  "CrossrefHandlerTest.txt"

end



def  executeDataDrivenTest  filename

    dataFile  =  File.open(filename)

    dataFile.each_line  do  |  line  |

        desc,  action,  part2  =  line.split(",")

          sourceXml,  expectedHtml,  leftOver  =  part2.split(",")

            if  "crossref"==action.strip

              generateAndVerifyHtml  sourceXml,  expectedHtml,  desc

        else  #  new  "verbs"  go  before  here  as  elsif's

              report_error(  "unknown  action"  +  action.strip  )

        end

    end

end

 

除非我们改变实现generateAndVerifyHtml以捕获断言失败并增加失败计数器,否则此数据驱动测试将在第一次失败的断言时停止执行。虽然这种行为对于回归测试来说是可以接受的,但它不会提供很好的缺陷定位

Unless we changed the implementation of generateAndVerifyHtml to catch assertion failures and increment a failure counter, this Data-Driven Test will stop executing at the first failed assertion. While this behavior would be acceptable for regression testing, it would not provide very good Defect Localization.

示例:使用 Fit 框架进行数据驱动测试

Example: Data-Driven Test Using Fit Framework

如果我们想要对用户的操作有更多控制,我们可以创建一个 Fit“列固定装置”,其中包含“id”、“action”、“源 XML”和“预期 Html()”列,然后让用户编辑 HTML 网页(图 18.1)。

If we wanted to have even more control over what the user can do, we could create a Fit "column fixture" with the columns "id," "action," "source XML," and "expected Html()" and let the user edit an HTML Web page instead (Figure 18.1).

图 18.1. 使用 Fit 框架构建的数据驱动测试。

Figure 18.1. A Data-Driven test built using the Fit framework.

图像

当使用 Fit 时,测试解释器是针对该测试而扩展的 Fit 框架:

When using Fit, the test interpreter is the Fit framework extended by the Fit fixture class specific to the test:

public class CrossrefHandlerFixture extends ColumnFixture {

      // 输入列

      public String id;

      public String action;

      public String sourceXML;



      // 输出列

      public String expectedHtml() {

            return generateHtml(sourceXML);

      }

}

public  class  CrossrefHandlerFixture  extends  ColumnFixture  {

      //  Input  columns

      public  String  id;

      public  String  action;

      public  String  sourceXML;



      //  Output  columns

      public  String  expectedHtml()  {

            return  generateHtml(sourceXML);

      }

}

 

Fit 框架会根据列标题为 Fit 表中每行中的每个单元格调用此 Fixture 类的方法。简单名称被解释为Fixture 的实例变量(例如,“id”、“source XML”)。以“()”结尾的列名表示 Fit 调用的函数,然后将结果与单元格的内容进行比较。

The methods of this fixture class are called by the Fit framework for each cell in each line in the Fit table based on the column headers. Simple names are interpreted as the instance variable of the fixture (e.g., "id," "source XML"). Column names ending in "()" signify a function that Fit calls and then compares its result with the contents of the cell.

最终的输出如图 18.2所示。通过这个彩色表格,我们可以一眼就看到运行一个测试文件的结果。

The resulting output is shown in Figure 18.2. This colored-in table allows us to get an overview of the results of running one file of tests at a single glance.

图 18.2. 执行拟合度检验的结果。

Figure 18.2. The results of executing the Fit test.

图像

测试自动化框架

Test Automation Framework

我们如何才能轻松编写和运行由不同人编写的测试?

How do we make it easy to write and run tests written by different people?

我们使用一个框架,它提供了运行测试逻辑所需的所有机制,因此测试编写者只需要提供特定于测试的逻辑。

We use a framework that provides all the mechanisms needed to run the test logic so the test writer needs to provide only the test-specific logic.

图像

编写和运行自动化测试涉及多个步骤,但其中许多步骤对于每个测试都是相同的。如果每个测试都必须包括这些步骤的实现,那么编写自动化测试将非常繁琐、耗时、容易出错且成本高昂。

Writing and running automated tests involves several steps, but many of these steps are the same for every test. If every test had to include an implementation of these steps, writing automated tests would be very tedious, time-consuming, prone to errors, and expensive.

使用测试自动化框架是最大限度地减少编写全自动测试工作量的一种方法(参见第 26页)。

Using a Test Automation Framework is a way to minimize the effort of writing Fully Automated Tests (see page 26).

工作原理

How It Works

我们构建了一个框架,该框架实现了运行测试套件并记录结果所需的所有机制。这些机制包括查找单个测试、将它们组装成测试套件、依次执行每个测试、验证预期结果、收集和报告任何测试失败或错误以及在发生失败或错误时进行清理的能力。该框架提供了一种插入和运行测试自动化程序编写的测试特定行为的方法。

We build a framework that implements all the mechanisms required to run suites of tests and record the results. These mechanisms include the ability to find individual tests, assemble them into a test suite, execute each test in turn, verify expected outcomes, collect and report any test failures or errors, and clean up when failures or errors do occur. The framework provides a way to plug in and run the test-specific behavior that test automaters write.

我们为什么这样做

Why We Do This

构建可重复且强大的全自动测试是一个比编写调用 SUT 的测试脚本复杂得多的过程。我们需要处理成功案例和错误案例,包括预期和意外的案例。我们需要设置和拆除测试装置。我们需要指定要运行哪些测试。我们还需要在运行一组测试后报告结果。

Building Fully Automated Tests that are repeatable and robust is a much more complicated process than just writing a test script that invokes the SUT. We need to handle success cases and error cases, both expected and unexpected. We need to set up and tear down test fixtures. We need to specify which test(s) to run. We also need to report on the results after we have run a suite of tests.

构建完全自动化测试所需的工作量可能会严重阻碍测试自动化。我们可以通过提供一个实现最常见功能的框架来显著降低入门成本 — 唯一的入门成本是在学习使用该框架时产生的。反过来,如果框架实现一个通用协议(如 xUnit),那么这笔成本也可以降低,这使得我们在掌握了第一个框架的经验后,可以更轻松地学习第二个或第三个框架。

The amount of effort required to build Fully Automated Tests can act as a serious deterrent to automation of tests. We can reduce the cost of getting started significantly by providing a framework that implements the most common functionality—the only entry cost is then incurred while learning to use the framework. This cost, in turn, can be reduced if the framework implements a common protocol such as xUnit that makes it easier for us to learn a second or third framework once we have experience with the first.

使用框架还有助于将运行测试所需的逻辑实现与测试逻辑隔离开来。这种方法有助于减少测试代码重复第 213页)并最大限度地减少模糊测试的发生(第 186页)。它还确保由不同的测试自动化人员编写的测试可以在一次测试运行中轻松运行,并生成一份测试结果报告。

Using a framework also helps isolate the implementation of the logic required to run the tests from the logic of the tests. This approach can help reduce Test Code Duplication (page 213) and minimize the occurrence of Obscure Tests (page 186). It also ensures that test written by different test automaters can be run easily in a single test run with a single report on the test results.

实施说明

Implementation Notes

市场上有许多种测试自动化框架,既有商业供应商提供的,也有开源资源提供的。它们可以分为两大类:“机器人用户”测试工具和脚本测试(第285页)。后一类可以进一步细分为 xUnit 和数据驱动测试第 288页)系列测试自动化框架

Many kinds of Test Automation Frameworks are available, from both commercial vendors and open-source resources. They can be classified into two main categories: "robot user" test tools and Scripted Tests (page 285). The latter category can be further subdivided into the xUnit and Data-Driven Tests (page 288) families of Test Automation Frameworks.

变体:机器人用户测试框架

大量第三方测试自动化工具旨在通过用户界面测试应用程序。它们中的大多数都使用“记录和回放”测试隐喻。这个隐喻带来了一些非常诱人的营销材料,因为它使测试自动化看起来就像在记录测试会话的同时手动运行一些测试一样简单。这样的机器人用户测试工具由两个主要部分组成:“测试记录器”,用于监视和记录用户与 SUT 之间的交互,以及“测试运行器”,用于执行记录的测试第 278页)。这些测试自动化工具中的大多数也是支持许多“小部件识别器”插件的框架。大多数商业工具都带有一组内置的小部件识别器。

A large number of third-party test automation tools are designed to test applications via the user interface. Most of them use the "record and playback" test metaphor. This metaphor leads to some very seductive marketing materials, because it makes test automation seem as simple as running some tests manually while recording the test session. Such a robot user test tool consists of two major parts: the "test recorder," which monitors and records the interactions between the user and the SUT, and the "test runner," which executes the Recorded Tests (page 278). Most of these test automation tools are also frameworks that support a number of "widget recognizer" plug-ins. Most commercial tools come with a gaggle of built-in widget recognizers.

变体:xUnit 系列测试自动化框架

大部分单元测试工具属于 xUnit 系列测试框架,旨在实现手动脚本测试的自动化(请参阅脚本测试)。xUnit 已被移植到(或从头开发为)大部分当前编程语言。xUnit 系列单元测试框架包含几个主要组件。最明显的是测试运行器(第 377页),它可以从命令行调用,也可以作为图形测试运行器调用(请参阅测试运行器)。它构建测试用例对象(第 382页),将它们收集到测试套件对象(第 387页) 中,并调用每个测试方法(第 348页)。xUnit 框架的另一个主要组件是内置断言方法库(第 362页),它在测试方法中使用来指定每个测试的预期结果。

Most unit-testing tools belong to the xUnit family of testing frameworks designed for automating Hand-Scripted Tests (see Scripted Test). xUnit has been ported to (or developed from scratch for) most current programming languages. The xUnit family of unit-testing frameworks consists of several major components. The most visible is the Test Runner (page 377), which can be invoked either from the command line or as a Graphical Test Runner (see Test Runner). It builds the Testcase Objects (page 382), collects them into Test Suite Objects (page 387), and invokes each of the Test Methods (page 348). The other major component of the xUnit frameworks is the library of built-in Assertion Methods (page 362) that are used within the Test Methods to specify the expected outcome of each test.

变体:数据驱动的测试框架

数据驱动测试框架提供了一种插入解释器的方法,这些解释器知道如何执行特定类型的测试步骤。这种灵活性实际上用新的“动词”和对象扩展了输入文件的格式。这样的框架还提供了一个测试运行器,它读取文件,在遇到相应的数据格式时将控制权交给插件,并跟踪测试运行的统计数据。数据驱动测试框架系列中最值得注意的成员是 Fit,它使测试自动化人员能够以表格形式编写测试,并“插入”知道如何解释特定格式表格的装置类。

A Data-Driven Test framework provides a way to plug in interpreters that know how to execute a specific kind of test step. This flexibility, in effect, extends the format of the input file with new "verbs" and objects. Such a framework also provides a test runner that reads in the file, passes control to the plug-ins when their corresponding data formats are encountered, and keeps track of statistics for the test run. The most notable member of the Data-Driven Test Frameworks family is Fit, which enables test automaters to write tests in tabular form and to "plug in" fixture classes that know how to interpret specific formats of tables.

示例:测试自动化框架

Example: Test Automation Framework

对于每种可能的自动化测试方式,测试自动化框架看起来都有所不同。要查看这些变化,请参阅记录测试脚本测试数据驱动测试,了解各个测试自动化框架的示例。

The Test Automation Framework looks somewhat different for each of the possible ways to automate tests. To see these variations, refer to Recorded Test, Scripted Test, and Data-Driven Test for examples of the respective Test Automation Frameworks.

进一步阅读

xUnit 的一些更流行的测试自动化框架示例包括 JUnit (Java)、SUnit (Smalltalk)、CppUnit (C++)、NUnit (所有 .NET 语言)、runit (Ruby)、PyUnit (Python) 和VbUnit (Visual Basic)。可以在http://xprogramming.com找到更完整和最新的列表,以及可用扩展的列表(例如 HttpUnit、Cactus)。

Some of the more popular examples of Test Automation Frameworks for xUnit are JUnit (Java), SUnit (Smalltalk), CppUnit (C++), NUnit (all .NET languages), runit (Ruby), PyUnit (Python), and VbUnit (Visual Basic). A more complete and up-to-date list can be found at http://xprogramming.com, along with a list of the available extensions (e.g., HttpUnit, Cactus).

其他开源测试自动化框架包括 Fit、Canoo WebTest 和Watir。商业测试自动化框架包括 QTP、BPT 和eCATT等。

Other open-source Test Automation Frameworks include Fit, Canoo WebTest, and Watir. Commercial Test Automation Frameworks include QTP, BPT, and eCATT, among many others.

《测试驱动开发——示例》 [TDD-BE] 中,Kent Beck通过使用Python构建测试自动化框架来说明TDD 。他用一种比作“对自己进行脑部手术”的方法,使用新兴的测试自动化框架来运行他为每个新功能编写的测试。此应用程序是 TDD 和引导的一个很好的例子。

In Test-Driven Development—By Example [TDD-BE], Kent Beck illustrates TDD by building a Test Automation Framework in Python. In an approach he likens to "doing brain surgery on yourself," he uses the emerging Test Automation Framework to run the tests he writes for each new capability. This application is a very good example of both TDD and bootstrapping.

最小装置

Minimal Fixture

也称为

Also known as

最小上下文

Minimal Context

我们应该使用哪种固定策略?

Which fixture strategy should we use?

我们在每个测试中都使用尽可能最小、最简单的装置。

We use the smallest and simplest fixture possible for each test.

图像

每个测试都需要某种测试装置。理解测试的一个关键部分是理解测试装置并认识到它如何影响测试的预期结果。如果装置小而简单,测试就更容易理解。

Every test needs some kind of test fixture. A key part of understanding a test is understanding the test fixture and recognizing how it influences the expected outcome of the test. Tests are much easier to understand if the fixture is small and simple.

我们为什么这样做

Why We Do This

最小夹具对于实现测试文档化(参见第 23页)和避免慢速测试第 253页)非常重要。使用最小夹具的测试总是比使用包含不必要或无关信息的夹具的测试更容易理解。无论我们使用新鲜夹具第 311页)还是共享夹具第 317页),情况都是如此,尽管使用共享夹具构建最小夹具的工作量通常更高,因为它必须设计为处理多个测试。对于新鲜夹具,定义最小夹具要容易得多,因为它只需要服务于一个测试。

A Minimal Fixture is important for achieving Tests as Documentation (see page 23) and for avoiding Slow Tests (page 253). A test that uses a Minimal Fixture will always be easier to understand than one that uses a fixture containing unnecessary or irrelevant information. This is true whether we are using a Fresh Fixture (page 311) or a Shared Fixture (page 317), although the effort to build a Minimal Fixture is typically higher with a Shared Fixture because it must be designed to handle several tests. Defining a Minimal Fixture is much easier for a Fresh Fixture because it need serve only a single test.

实施说明

Implementation Notes

我们设计了一个仅包含那些对于表达测试验证的行为绝对必要的对象的夹具。另一种表述方式是“如果对象对于理解测试并不重要,那么就不要将其包含在夹具中。”

We design a fixture that includes only those objects that are absolutely necessary to express the behavior that the test verifies. Another way to phrase this is "If the object is not important to understand the test, it is important not to include it in the fixture."

为了构建最小夹具,我们会毫不留情地从夹具中删除所有无助于测试传达​​ SUT 应如何表现的内容。可以考虑两种形式的“最小化”:

To build a Minimal Fixture, we ruthlessly remove anything from the fixture that does not help the test communicate how the SUT should behave. Two forms of "minimization" can be considered:

  • 我们可以完全消除对象。也就是说,我们甚至不将对象构建为装置的一部分。如果对象对于证明 SUT 的行为方式而言不是必需的,我们根本不会将其包括在内。
  • We can eliminate objects entirely. That is, we don't even build the objects as part of the fixture. If the object isn't necessary to prove something about how the SUT behaves, we don't include it at all.
  • 当对象的不必要属性无助于理解预期行为时,我们可以隐藏它们。
  • We can hide unnecessary attributes of the object when they don't contribute to the understanding of the expected behavior.

一个简单的方法来找出一个对象是否是基座必要的一部分是删除它。如果测试因此失败,该对象可能在某种程度上是必要的。当然,它可能只是作为某些我们不感兴趣的方法的参数,或者作为从未使用过的属性(即使由于某种原因需要该属性所属的对象)才是必要的。将这些类型的对象作为基座设置的一部分肯定会导致模糊测试第 186页)。我们可以通过以下两种方式之一消除这些不必要的对象:(1)隐藏它们或(2)通过传入虚拟对象(第728页)或使用实体链剪切(请参阅第 529页的测试桩)来消除对它们的需要。但是,如果 SUT 在执行测试逻辑时实际访问了该对象,我们可能不得不将该对象作为测试基座的一部分。

A simple way to find out whether an object is necessary as part of the fixture is to remove it. If the test fails as a result, the object was probably necessary in some way. Of course, it may have been necessary only as an argument to some method we are not interested in or as an attribute that is never used (even though the object to which the attribute belongs is required for some reason). Including these kinds of objects as part of fixture setup definitely contributes to Obscure Tests (page 186). We can eliminate these unnecessary objects in one of two ways: (1) by hiding them or (2) by eliminating the need for them by passing in Dummy Objects (page 728) or using Entity Chain Snipping (see Test Stub on page 529). If the SUT actually accesses the object as it is executing the logic under test, however, we may be forced to include the object as part of the test fixture.

确定对象对于测试的执行是必需的之后,我们现在必须问一问,该对象是否有助于理解测试。如果我们在“后台”初始化它,这是否会使测试更难理解?该对象是否会通过充当神秘嘉宾而导致模糊测试(请参阅模糊测试)?如果是这样,我们希望让对象保持可见。边界值是一个很好的例子,在这种情况下,我们确实希望让采用边界值的对象和属性保持可见。

Having determined that the object is necessary for the execution of the test, we must now ask whether the object is helpful in understanding the test. If we were to initialize it "off-stage," would that make it harder to understand the test? Would the object lead to an Obscure Test by acting as a Mystery Guest (see Obscure Test)? If so, we want to keep the object visible. Boundary values are a good example of a case in which we do want to keep the objects and attributes that take on the boundary values visible.

如果我们已经确定对象或属性对于理解测试不是必需的,我们应该尽一切努力将其从测试方法(第348页) 中消除,尽管不一定从测试装置中消除。创建方法(第 415页) 是实现此目标的常用方法。我们可以隐藏不影响测试结果但构造对象所需的对象的属性,方法是使用创建方法用有意义的默认值填充所有“不关心”的属性。我们还可以在创建方法中隐藏必要的依赖对象的创建。一个很好的例子是,当我们编写需要格式错误的对象作为输入的测试时(用于使用无效输入测试 SUT)。在这种情况下,我们不想通过显示传递给 SUT 的对象的所有有效属性来混淆问题;可能有许多这样的无关属性。相反,我们希望专注于无效属性。为此,我们可以使用“一个错误属性”模式(请参阅第 718页的“派生值” ),通过调用创建方法来构造有效对象,然后用无效值替换单个属性,以最少的代码构建格式错误的对象,我们希望验证 SUT 是否可以正确处理。

If we have established that the object or attribute isn't necessary for understanding the test, we should make every effort to eliminate it from the Test Method (page 348), albeit not necessarily from the test fixture. Creation Methods (page 415) are a common way of achieving this goal. We can hide the attributes of objects that don't affect the outcome of the test but that are needed for construction of the object by using Creation Methods to fill in all the "don't care" attributes with meaningful default values. We can also hide the creation of necessary depended-on objects within the Creation Methods. A good example of this occurs when we write tests that require badly formed objects as input (for testing the SUT with invalid inputs). In this case we don't want to confuse the issue by showing all valid attributes of the object being passed to the SUT; there could be many of these extraneous attributes. Instead, we want to focus on the invalid attribute. To do so, we can use the One Bad Attribute pattern (see Derived Value on page 718) to build malformed objects with a minimum of code by calling a Creation Method to construct a valid object and then replacing a single attribute with the invalid value that we want to verify the SUT will handle correctly.

标准夹具

Standard Fixture

也称为

Also known as

标准上下文

Standard Context

我们应该使用哪种固定策略?

Which fixture strategy should we use?

我们在许多测试中重复使用测试夹具的设计。

We reuse the design of the test fixture across the many tests.

图像

要执行自动化测试,我们需要一个易于理解且完全确定的文本装置。为每个测试设计自定义测试装置需要额外的努力。标准装置提供了一种在多个测试中重复使用相同装置设计的方法,而不必共享相同的装置实例

To execute an automated test, we require a text fixture that is well understood and completely deterministic. Designing a custom test fixture for each test requires extra effort. A Standard Fixture offers a way to reuse the same fixture design in several tests without necessarily sharing the same fixture instance.

工作原理

How It Works

标准夹具更多的是一种态度,而不是技术。它要求我们在测试过程的早期就决定,我们将设计一个可供多个或多个测试使用的标准夹具,而不是从独立设计的测试中挖掘出一个通用夹具。从某种意义上说,标准夹具是整个测试套件的测试夹具“预先进行大规模设计”的结果。然后,我们使用这个通用测试夹具设计来定义我们的具体测试。

A Standard Fixture is more about attitude than about technology. It requires us to decide early on in the testing process that we will design a Standard Fixture that can be used by several or many tests rather than mining a common fixture from tests that were designed independently. In a sense, a Standard Fixture is the result of "Big Design Upfront" of the test fixture for a whole suite of tests. We then define our specific tests using this common test fixture design.

标准夹具的选择与新夹具(第 311页) 和共享夹具(第 317页)之间的选择无关。根据定义,共享夹具就是标准夹具。但反之则不然,因为标准夹具注重夹具设计的重用— 而不是夹具的构建时间或其可见性。选择使用标准夹具后,我们仍需要决定每个测试是否构建自己的标准夹具实例(新夹具),或者我们是否将其构建一次作为共享夹具并在多个测试中重用它。

The choice of a Standard Fixture is independent of the choice between a Fresh Fixture (page 311) and a Shared Fixture (page 317). A Shared Fixture is, by definition, a Standard Fixture. The reverse is not true, however, because a Standard Fixture focuses on reuse of the fixture's design—not the time when the fixture is built or its visibility. Having chosen to use a Standard Fixture, we still need to decide whether each test will build its own instance of the Standard Fixture (a Fresh Fixture) or whether we will build it once as a Shared Fixture and reuse it across many tests.

何时使用它

When to Use It

当我与丛书编辑 Martin Fowler 一起审阅本书的初稿时,他问我:“人们真的会这样做吗?”这个问题体现了 Fixture 设计的哲学分歧。Martin 拥有敏捷开发背景,他让每个测试都拥有一个 Fixture。如果多个测试恰好需要同一个 Fixture,那么将其分解到方法中并将类拆分为每个 Fixture 的一个测试用例(第 631页) 是有意义的。Martin 甚至没有想到要设计一个所有测试都可以使用的标准 Fixture。那么谁会使用它们呢?setUp

When I was reviewing an early draft of this book with Series Editor Martin Fowler, he asked me, "Do people actually do this?" This question exemplifies the philosophical divide of fixture design. Coming from an agile background, Martin lets each test pull a fixture into existence. If several tests happen to need the same fixture, then it makes sense to factor it out into the setUp method and split the class into one Testcase Class per Fixture (page 631). It doesn't even occur to Martin to design a Standard Fixture that all tests can use. So who uses them?

标准装置是测试(质量评估)社区的传统。定义一个大型标准装置,然后将其用作测试活动的测试平台,这是非常常见的做法。这种方法在手动执行许多客户测试的情况下非常有意义,因为它消除了每个测试人员花费大量时间为每个客户测试设置测试环境的需要,并且允许多个测试人员同时在同一个测试环境中工作。一些测试自动化人员在定义自动化客户测试时也会使用标准装置。出于显而易见的原因,当测试自动化人员使用共享装置时,这种策略尤其普遍

Standard Fixtures are something of a tradition in the testing (quality assessment) community. It is very commonplace to define a large Standard Fixture that is then used as a test bed for testing activities. This approach makes a lot of sense in the context of manual execution of many customer tests because it eliminates the need for each tester to spend a lot of time setting up the test environment for each customer test and it allows several testers to work in the same test environment at the same time. Some test automaters also use Standard Fixtures when defining their automated customer tests. This strategy is especially prevalent when test automaters use a Shared Fixture, for obvious reasons.

在 xUnit 社区中,为了避免为每个测试设计一个最小夹具(第 302页) 而使用标准夹具被认为是不可取的,因此将其命名为通用夹具(请参阅第186页的模糊测试)。一个更被接受的例子是将隐式设置(第424页) 与每个夹具的测试用例类结合使用,因为只有少数测试方法(第 348页) 共享夹具的设计,而它们这样做是因为它们需要相同的设计。随着我们使标准夹具在具有不同需求的许多测试中可重用,它往往会变得更大更复杂。这种趋势可能导致脆弱夹具(请参阅第 239页的脆弱测试),因为新测试的需求会引入更改,从而破坏标准夹具的现有客户端。根据我们构建标准夹具的方式,如果夹具和结果之间的因果关系不易辨别,或者因为夹具设置在测试中被隐藏,或者因为不清楚标准夹具参考部分的哪些特性是测试的先决条件,我们可能还会发现自己招待了一位神秘嘉宾(参见模糊测试)。

In the xUnit community, use of a Standard Fixture simply to avoid designing a Minimal Fixture (page 302) for each test is considered undesirable and has been given the name General Fixture (see Obscure Test on page 186). A more accepted example is the use of Implicit Setup (page 424) in conjunction with Testcase Class per Fixture because only a few Test Methods (page 348) share the design of the fixture and they do so because they need the same design. As we make a Standard Fixture more reusable across many tests with disparate needs, it tends to grow larger and more complex. This trend can lead to a Fragile Fixture (see Fragile Test on page 239) as the needs of new tests introduce changes that break existing clients of the Standard Fixture. Depending on how we go about building the Standard Fixture, we may also find ourselves entertaining a Mystery Guest (see Obscure Test) if the cause–effect relationships between the fixture and outcome are not easy to discern either because the fixture setup is hidden from the test or because it is not clear which characteristics of the referenced part of the Standard Fixture serve as pre-conditions for the test.

标准Fixture 的构建时间也比最小 Fixture长,因为需要构建更多的 Fixture。当我们为每个测试用例对象第 382页)构建一个新 Fixture时,这种努力可能会导致测试速度变慢第 253页),尤其是当 Fixture 设置涉及数据库时。(请参阅侧栏“单元测试规则”,了解单元测试可以接受哪些类型的行为。)出于这些原因,我们最好使用最小 Fixture,以避免与创建仅在其他测试中需要的对象相关的额外 Fixture 设置开销。

A Standard Fixture will also take longer to build than a Minimal Fixture because there is more fixture to construct. When we are building a Fresh Fixture for each Testcase Object (page 382), this effort can lead to Slow Tests (page 253), especially if the fixture setup involves a database. (See the sidebar "Unit Test Rulz" for an opinion about what kinds of behavior are acceptable for a unit test.) For these reasons, we may be better off using a Minimal Fixture to avoid the extra fixture setup overhead associated with creating objects that are only needed in other tests.


单元测试规则

Object Mentor 的 Michael Feathers 写道:

我已在大量团队中使用过这些规则。它们鼓励良好的设计和快速反馈,似乎可以帮助团队避免很多麻烦。

如果出现以下情况,则测试不是单元测试:

 
  • 它与数据库对话。
  • 它通过网络进行通信。
  • 它触及文件系统。
  • 它不能与任何其他单元测试同时正确运行。
  • 您必须对您的环境做一些特殊的事情(例如编辑配置文件)才能运行它。

执行这些操作的测试并不坏。它们通常值得编写,并且可以在单元测试工具中编写。但是,重要的是能够将它们与真正的单元测试分开,以便我们可以保留一组可以在进行更改时快速运行的测试。

 

http://www.objectmentor.com



Unit Test Rulz

Michael Feathers of Object Mentor writes:

I've used these rules with a large number of teams. They encourage good design and rapid feedback and they seem to help teams avoid a lot of trouble.

A test is not a unit test if:

 
  • It talks to the database.
  • It communicates across the network.
  • It touches the file system.
  • It can't run correctly at the same time as any of your other unit tests.
  • You have to do special things to your environment (such as editing config files) to run it.

Tests that do these things aren't bad. Often they are worth writing, and they can be written in a unit test harness. However, it is important to be able to separate them from true unit tests so that we can keep a set of tests that we can run fast whenever we make our changes.

 

http://www.objectmentor.com


 

实施说明

Implementation Notes

如前所述,我们可以使用标准夹具作为新鲜夹具共享夹具,并且我们可以使用隐式设置委托设置第 411页)进行设置。 7当将其用作新鲜夹具时,我们可以定义一个测试实用程序方法(第 599页)(函数或过程)来构建标准夹具;然后我们可以从需要此特定夹具设计的每个测试中调用测试实用程序方法。 或者,我们可以利用 xUnit 对隐式设置的支持,将所有夹具构造逻辑放入setUp方法中。

As mentioned earlier, we can use a Standard Fixture as either a Fresh Fixture or a Shared Fixture, and we can set it up using either Implicit Setup or Delegated Setup (page 411).7 When using it as a Fresh Fixture, we can define a Test Utility Method (page 599) (function or procedure) that builds the Standard Fixture; we can then call the Test Utility Method from each test that needs this particular design of fixture. Alternatively, we can take advantage of xUnit support for Implicit Setup by putting all of the fixture construction logic in the setUp method.

当构建一个标准 Fixture用作共享 Fixture时,我们可以使用任何共享 Fixture设置模式,包括Suite Fixture Setup第 441页)、Lazy Setup第 435页)和Setup Decorator第 447页)。

When building a Standard Fixture for use as a Shared Fixture, we can employ any of the Shared Fixture setup patterns including Suite Fixture Setup (page 441), Lazy Setup (page 435), and Setup Decorator (page 447).

激励人心的例子

Motivating Example

如前所述,我们很可能最终使用标准 Fixture,因为我们一开始就是这样的 — 而且我们之所以这样开始,可能是因为项目参与者之一的背景。如果测试已经编写为使用最小Fixture,我们可能不会重构测试以使用标准Fixture ,除非我们重构以为每个 Fixture 创建一个测试用例类。为了便于说明,我们假设我们确实想从“那里”到达“这里”。以下示例使用创建方法(第415页)为每个测试构建自定义新鲜 Fixture

As mentioned earlier, we are most likely to end up using a Standard Fixture because we started that way—and we probably started that way as the result of the background of one of the project participants. We probably would not refactor our tests to use a Standard Fixture when those tests are already written to use a Minimal Fixture unless we were refactoring to create a Testcase Class per Fixture. For the sake of illustration, let's assume that we did want to get to "here" from "there." The following example uses Creation Methods (page 415) to build a custom Fresh Fixture for each test:

public void testGetFlightsByFromAirport_OneOutboundFlight_c()

              throws Exception {

      FlightDto outboundFlight = createOneOutboundFlightDto();

      // 练习系统

      列表 flightsAtOrigin =

                Facade.getFlightsByOriginAirport(

                                        outboundFlight.getOriginAirportId());

      // 验证结果

      assertOnly1FlightInDtoList( "Flights at origin",

                                                outboundFlight,

                                                flightsAtOrigin);

}



public void testGetFlightsByFromAirport_TwoOutboundFlights_c()

              throws Exception {

      FlightDto[] outboundFlights =

        createTwoOutboundFlightsFromOneAirport();

      // 练习系统

      列表 flightsAtOrigin = Facade.getFlightsByOriginAirport(

                              outboundFlights[0].getOriginAirportId());

      // 验证结果

      assertExactly2FlightsInDtoList( "Flights at origin",

                                                   outboundFlights,

                                                   flightsAtOrigin);

}

public  void  testGetFlightsByFromAirport_OneOutboundFlight_c()

              throws  Exception  {

      FlightDto  outboundFlight  =  createOneOutboundFlightDto();

      //  Exercise  System

      List  flightsAtOrigin  =

                facade.getFlightsByOriginAirport(

                                        outboundFlight.getOriginAirportId());

      //  Verify  Outcome

      assertOnly1FlightInDtoList(  "Flights  at  origin",

                                                outboundFlight,

                                                flightsAtOrigin);

}



public  void  testGetFlightsByFromAirport_TwoOutboundFlights_c()

              throws  Exception  {

      FlightDto[]  outboundFlights  =

        createTwoOutboundFlightsFromOneAirport();

      //  Exercise  System

      List  flightsAtOrigin  =  facade.getFlightsByOriginAirport(

                              outboundFlights[0].getOriginAirportId());

      //  Verify  Outcome

      assertExactly2FlightsInDtoList(  "Flights  at  origin",

                                                   outboundFlights,

                                                   flightsAtOrigin);

}

 

为了使此测试简短,我们使用了委托设置来向 SUT 填充每个测试所需的最小装置。我们本可以在每个方法中内联包含装置设置代码,但这种选择会让我们陷入模糊测试的泥潭

To keep this test short, we have used Delegated Setup to populate the SUT with the Minimal Fixture needed for each test. We could have included the fixture setup code in-line in each method, but that choice would take us down the road toward an Obscure Test.

重构说明

Refactoring Notes

从技术上讲,将一堆测试转换为标准装置并不是真正的“重构”,因为我们实际上改变了这些测试的行为。最大的挑战是设计可重复使用的标准装置,以便每个测试方法都能找到装置中满足其需求的某些部分。这意味着将所有单独的专用最小装置合成为一个“万能”装置。毫不奇怪,当我们有大量测试时,这种代码重写可能是一项不简单的工作。

Technically speaking, converting a pile of tests to a Standard Fixture isn't really a "refactoring" because we actually change the behavior of these tests. The biggest challenge is designing the reusable Standard Fixture in such a way that each Test Method can find some part of the fixture that serves its needs. This means synthesizing all of the individual purpose-built Minimal Fixtures into a single "jack of all trades" fixture. Not surprisingly, this reworking of the code can be a nontrivial exercise when we have a lot of tests.

重构的简单而机械的部分是将每个测试中构建夹具的逻辑转换为对Finder 方法的调用(参见测试实用程序方法),这些调用检索标准夹具的适当部分。这种转换最容易通过一系列步骤完成。首先,我们将每个测试方法中的内联夹具构造逻辑提取到一个或多个具有意图显示名称的创建方法[SBPP]中。接下来,我们对每个对“find”的调用的“create”部分进行全局替换。最后,我们生成(手动或使用我们 IDE 的“快速修复”功能)编译调用所需的Finder 方法。在每个Finder 方法中,我们添加代码以返回标准夹具的相关部分。

The easy and mechanical part of the refactoring is to convert the logic in each test that constructs the fixture into calls to Finder Methods (see Test Utility Method) that retrieve the appropriate part of the Standard Fixture. This transformation is most easily done as a series of steps. First, we extract the in-line fixture construction logic in each Test Method into one or more Creation Methods with Intent-Revealing Names [SBPP]. Next, we do a global replace on the "create" part of each call to "find." Finally, we generate (either manually or using our IDE's "quick fix" capability) the Finder Methods needed to get the calls to compile. Inside each Finder Methods we add in code to return the relevant part of the Standard Fixture.

例如:标准夹具

Example: Standard Fixture

以下是先前转换为使用标准夹具的示例:

Here's the example given earlier converted to use a Standard Fixture:

public void testGetFlightsByFromAirport_OneOutboundFlight()

                throws Exception {

      setupStandardAirportsAndFlights();

      FlightDto outboundFlight = findOneOutboundFlight();

      // 练习系统

      列表 flightsAtOrigin = Facade.getFlightsByOriginAirport(

                           outboundFlight.getOriginAirportId());

      // 验证结果

      assertOnly1FlightInDtoList( "Flights at origin",

                                               outboundFlight,

                                               flightsAtOrigin);

}



public void testGetFlightsByFromAirport_TwoOutboundFlights()

                throws Exception {

      setupStandardAirportsAndFlights();

      FlightDto[] outboundFlights =

                      findTwoOutboundFlightsFromOneAirport();

      // 练习系统

      列表 flightsAtOrigin = Facade.getFlightsByOriginAirport(

                            outboundFlights[0].getOriginAirportId());

      // 验证结果

      assertExactly2FlightsInDtoList( "出发地航班",

                                                   outboundFlights,

                                                   flightsAtOrigin);

}

public  void  testGetFlightsByFromAirport_OneOutboundFlight()

                throws  Exception  {

      setupStandardAirportsAndFlights();

      FlightDto  outboundFlight  =  findOneOutboundFlight();

      //  Exercise  System

      List  flightsAtOrigin  =  facade.getFlightsByOriginAirport(

                           outboundFlight.getOriginAirportId());

      //  Verify  Outcome

      assertOnly1FlightInDtoList(  "Flights  at  origin",

                                               outboundFlight,

                                               flightsAtOrigin);

}



public  void  testGetFlightsByFromAirport_TwoOutboundFlights()

                throws  Exception  {

      setupStandardAirportsAndFlights();

      FlightDto[]  outboundFlights  =

                      findTwoOutboundFlightsFromOneAirport();

      //  Exercise  System

      List  flightsAtOrigin  =  facade.getFlightsByOriginAirport(

                            outboundFlights[0].getOriginAirportId());

      //  Verify  Outcome

      assertExactly2FlightsInDtoList(  "Flights  at  origin",

                                                   outboundFlights,

                                                   flightsAtOrigin);

}

 

为了使标准夹具的使用更加明显,此示例展示了一个在每个测试中通过调用相同的创建方法来设置标准夹具(即使用委托设置)明确创建的新鲜夹具。我们可以通过将夹具构造逻辑放入方法中来实现相同的效果,从而使用隐式设置。生成的测试看起来与使用共享夹具的测试相同。setUp

To make the use of a Standard Fixture really obvious, this example shows a Fresh Fixture that is created explicitly in each test by calling the same Creation Method to set up the Standard Fixture (i.e., using Delegated Setup). We could have achieved the same effect by putting the fixture construction logic into the setUp method, thus using Implicit Setup. The resulting test would look identical to one that uses a Shared Fixture.

新鲜装置

Fresh Fixture

也称为

Also known as

清新环境,私密空间

Fresh Context, Private Fixture

我们应该使用哪种固定策略?

Which fixture strategy should we use?

每个测试都会构建自己的全新测试装置,供自己私人使用。

Each test constructs its own brand-new test fixture for its own private use.

图像

每个测试都需要一个测试装置。它定义了测试前测试环境的状态。每次运行测试时是否从头开始构建装置或重复使用先前构建的装置是一个关键的测试自动化决策。

Every test needs a test fixture. It defines the state of the test environment before the test. The choice of whether to build the fixture from scratch each time the test is run or to reuse a fixture built earlier is a key test automation decision.

当每个测试创建一个新的夹具时,不稳定测试第 228页)的可能性就会减小,并且测试工作更有可能产生作为文档的测试(参见第23页)。

When each test creates a Fresh Fixture, Erratic Tests (page 228) are less likely and the testing effort is more likely to result in Tests as Documentation (see page 23).

工作原理

How It Works

我们设计和构建测试装置,以便只有单个测试的一次运行才会使用它。我们在运行测试的过程中构建装置,并在测试完成后拆除装置。我们不会重复使用其他测试或其他测试运行留下的任何装置。这样,我们就可以以“干净的记录”开始和结束每个测试。

We design and build the test fixture such that only a single run of a single test will use it. We construct the fixture as part of running the test and tear down the fixture when the test has finished. We do not reuse any fixture left over by other tests or other test runs. This way, we start and end every test with a "clean slate."

何时使用它

When to Use It

我们应该使用Fresh Fixture来避免测试之间的相互依赖,因为这些依赖可能会导致不稳定的测试,例如单独测试(请参阅不稳定测试)或交互测试(请参阅不稳定测试)。如果我们不能使用Fresh Fixture因为它会大大降低测试速度,那么在使用Shared Fixture之前,我们应该考虑使用Immutable Shared Fixture(请参阅第317页的Shared Fixture ) 。请注意,使用数据库分区方案(请参阅第650页的数据库沙箱)为测试创建一个其他测试都不会触及的私有数据库沙箱不会导致Fresh Fixture,因为后续测试运行可以使用相同的 Fixture。

We should use a Fresh Fixture whenever we want to avoid any interdependencies between tests that can result in Erratic Tests such as Lonely Tests (see Erratic Test) or Interacting Tests (see Erratic Test). If we cannot use a Fresh Fixture because it slows the tests down too much, we should consider using an Immutable Shared Fixture (see Shared Fixture on page 317) before resorting to a Shared Fixture. Note that using a Database Partitioning Scheme (see Database Sandbox on page 650) to create a private Database Sandbox for the test that no other tests will touch does not result in a Fresh Fixture because subsequent test runs could use the same fixture.

实施说明

Implementation Notes

如果我们打算只使用一次 Fixture,则将其视为Fresh Fixture。Fresh Fixture是瞬态的还是持久的取决于 SUT 的性质以及测试的编写方式(图 18.3)。虽然意图相同,但当Fresh Fixture是持久的时,实现考虑会有所不同。Fixture 设置基本不受影响,因此将其作为所有此类 Fixture 的共同特征进行讨论。Fixture 拆卸特定于特定变体。

A fixture is considered a Fresh Fixture if we intend to use it a single time. Whether the Fresh Fixture is transient or persistent depends on the nature of the SUT and how the tests are written (Figure 18.3). While the intent is the same, the implementation considerations are somewhat different when the Fresh Fixture is persistent. Fixture setup is largely unaffected, so it is discussed as a feature common to all such fixtures. Fixture teardown is specific to the particular variation.

图 18.3. 测试装置策略。装置可以是新鲜的、共享的,也可以是两者的组合(不可变的共享装置),这取决于装置中的部分或全部在测试之间是否持续存在。

Figure 18.3. Test fixture strategies. A fixture can be either Fresh, Shared, or a combination of the two (the immutable Shared Fixture) based on whether some, or all, of it persists between tests.

图像

为什么 Fixture 会持续存在?

我们构建的 Fixture 可能会在测试方法(第 348页) 执行完毕后继续存在,原因有两个。首先,如果 Fixture 主要由 SUT 所依赖的其他对象或组件的状态组成,则其持久性取决于这些其他对象本身是否持久。数据库就是这样一个庞然大物。这是因为,一旦某些代码将 Fixture 对象持久化到数据库中,这些对象就会在测试完成后很长时间内“继续存在”。它们在数据库中的存在为我们自己的测试的多次运行之间发生冲突打开了大门 (不可重复的测试请参阅 不稳定的测试)。其他测试也可能能够访问这些对象,这可能导致其他形式的不稳定测试,例如交互测试测试运行战争。如果我们必须使用数据库或其他形式的对象持久性,我们应该采取额外措施来保持 Fixture 的私密性。此外,我们应该在每次测试方法之后拆除 Fixture 。

The fixture we construct may hang around after the Test Method (page 348) has finished executing for one of two reasons. First, if the fixture primarily consists of the state of some other objects or components on which the SUT depends, its persistence is determined by whether those other objects are themselves persistent. A database is one such beast. That's because as soon as some code persists the fixture objects into a database, the objects "hang around" long after our test is done. Their existence in the database opens the door to collisions between multiple runs of our own test (Unrepeatable Test; see Erratic Test). Other tests may also be able to access those objects, which can result in other forms of Erratic Tests such as Interacting Tests and Test Run Wars. If we must use a database or other form of object persistence, we should take extra steps to keep the fixture private. In addition, we should tear down the fixture after each Test Method.

Fixture 可能持久存在的第二个原因在于我们的测试控制范围内 — 即,我们选择哪种变量来保存 Fixture 的引用。当测试方法完成执行时,局部变量自然会超出范围;因此,任何保存在局部变量中的 Fixture 都将被垃圾回收器销毁。当Testcase Object被销毁8 时,实例变量会超出范围,并且仅当 xUnit 框架在每次测试运行期间不重新创建Testcase Object时才需要显式拆卸。相比之下,类变量通常会导致持久 Fixture 的寿命比单个测试方法甚至测试运行都长,因此在使用Fresh Fixture时应避免使用。

The second reason that a fixture might persist lies within the control of our tests—namely, which kind of variable we choose to hold the reference to the fixture. Local variables naturally go out of scope when the Test Method finishes executing; therefore any fixture held in a local variable will be destroyed by garbage collection. Instance variables go out of scope when the Testcase Object is destroyed8 and require explicit teardown only if the xUnit framework doesn't recreate the Testcase Objects during each test run. By contrast, class variables usually result in persistent fixtures that can outlive a single test method or even a test run and should therefore be avoided when using a Fresh Fixture.

实际上,除非我们将应用程序逻辑与数据库紧密耦合,否则我们的 Fixture 通常不会在单元测试9中持久化。当我们编写客户测试或可能的组件测试时,Fixture 更有可能持久化。

In practice, our fixture will not normally be persistent in unit tests9 unless we have tightly coupled our application logic to the database. A fixture is more likely to be persistent when we are writing customer tests or possibly component tests.

全新装置设置

无论是持久性还是瞬态,Fixture 的构建基本不受其影响。主要考虑因素是设置 Fixture 的代码位置。如果 Fixture 设置相对简单,我们可以使用内联设置(第408页)。对于更复杂的 Fixture,当我们的测试方法是使用每个类一个测试用例类(第 617页) 或每个特性一个测试用例类(第 624页) 来组织的时,我们通常更喜欢使用委托设置(第 411页)。如果我们已经使用了每个 Fixture 一个测试用例类(第 631页) 的组织方式,我们可以使用隐式设置(第 424页) 来构建 Fixture 。

Construction of the fixture is largely unaffected by whether it is persistent or transient. The primary consideration is the location of the code to set up the fixture. We can use In-line Setup (page 408) if the fixture setup is relatively simple. For more complex fixtures, we generally prefer using Delegated Setup (page 411) when our Test Methods are organized using Testcase Class per Class (page 617) or Testcase Class per Feature (page 624). We can use Implicit Setup (page 424) to build the fixture if we have used the Testcase Class per Fixture (page 631) organization.

变化:瞬时新鲜装置

如果需要在测试中的多个地方引用 Fixture,则应仅使用局部变量或实例变量来引用 Fixture。在大多数情况下,我们可以依靠垃圾收集拆卸第 500页)来销毁 Fixture,而无需我们付出任何努力。

If we need to refer to the fixture from several places in the test, we should use only local variables or instance variables to refer to the fixture. In most cases we can depend on Garbage-Collected Teardown (page 500) to destroy the fixture without any effort on our part.

请注意,如果在运行每个测试方法之前从头开始构建 Fixture,则标准 Fixture第 305页)也可以是新 Fixture 。这种方法重用了Fixture 的设计,而不是实例。当我们使用隐式设置但没有使用每个 Fixture 的测试用例类来组织我们的测试方法时,通常会遇到这种情况。

Note that a Standard Fixture (page 305) can also be a Fresh Fixture if the fixture is built from scratch before each Test Method is run. This approach reuses the design of the fixture rather than the instance. It is commonly encountered when we use Implicit Setup but we are not using Testcase Class per Fixture to organize our Test Methods.

变化:持久新鲜装置

如果我们最终使用持久性新鲜夹具,我们要么需要拆除夹具,要么需要采取特殊措施来避免拆除夹具。我们可以使用内联拆卸第 509页)、隐式拆卸第 516页)、委托拆卸(参见内联拆卸自动拆卸(第503页)来拆除夹具,以使测试环境保持与我们进入时相同的状态。

If we do end up using a Persistent Fresh Fixture, either we need to tear down the fixture or we need to take special measures to avoid the need for its teardown. We can tear down the fixture using In-line Teardown (page 509), Implicit Teardown (page 516), Delegated Teardown (see In-line Teardown), or Automated Teardown (page 503) to leave the test environment in the same state as when we entered it.

为了避免装置拆卸,我们可以为每个装置对象使用唯一的生成值(请参阅第 723页的生成值)。此策略可以成为数据库分区方案的基础,旨在将测试和测试运行器彼此隔离。如果我们的拆卸过程失败,它可以防止资源泄漏(请参阅不稳定测试)。我们还可以将这种方法与拆卸模式之一相结合,以确保不存在不可重复的测试交互测试

To avoid fixture teardown, we can use a Distinct Generated Value (see Generated Value on page 723) for each fixture object that must be unique. This strategy can become the basis of a Database Partitioning Scheme that seeks to isolate the tests and test runners from one another. It would prevent Resource Leakage (see Erratic Test) in case our teardown process fails. We can also combine this approach with one of the teardown patterns to be doubly sure that no Unrepeatable Tests or Interacting Tests exist.

毫不奇怪,这些额外的工作有一些缺点:它使测试编写起来更加复杂,并且经常导致测试速度变慢第 253页)。一种自然的反应是利用 Fixture 的持久性,在多个测试中重用它,从而避免设置和拆除它的开销。不幸的是,这种选择有很多不良后果,因为它违反了我们的主要原则之一:保持测试独立(参见第42页)。由此产生的共享 Fixture必然会导致交互测试不可重复测试,如果不是立即发生,那么在将来的某个时候。在没有充分了解后果的情况下,我们不应该冒险走这条路!

Not surprisingly, this additional work has some drawbacks: It makes tests more complicated to write and it often leads to Slow Tests (page 253). A natural reaction is to take advantage of the persistence of the fixture by reusing it across many tests, thereby avoiding the overhead of setting it up and tearing it down. Unfortunately, this choice has many undesirable ramifications because it violates one of our major principles: Keep Tests Independent (see page 42). The resulting Shared Fixture invariably leads to Interacting Tests and Unrepeatable Tests, if not immediately, then at some point down the road. We should not venture down this road without fully understanding the consequences!

激励人心的例子

Motivating Example

以下是共享装置的一个例子:

Here's an example of a Shared Fixture:

静态 Flight flight;

public void setUp() {

      if (flight == null) { // 惰性设置

          Airport leaveAirport = new Airport("Calgary", "YYC");

          Airport destAirport = new Airport("Toronto", "YYZ");

          flight = new Flight( flightNumber,

                                          leaveAirport,

                                          destAirport);

      }

}



public void testGetStatus_inital_S() {

      // 隐式设置

      // 执行 SUT 并验证结果

      assertEquals(FlightState.PROPOSED, flight.getStatus());

      // 拆卸

}

public void testGetStatus_cancelled() {

      // 隐式设置部分覆盖

      flight.cancel();

      // 执行 SUT 并验证结果

      assertEquals(FlightState.CANCELLED, flight.getStatus());

      // 拆卸

}

static  Flight  flight;

public  void  setUp()  {

      if  (flight  ==  null)  {    //  Lazy  SetUp

          Airport  departAirport  =  new  Airport("Calgary",  "YYC");

          Airport  destAirport  =  new  Airport("Toronto",  "YYZ");

          flight  =  new  Flight(  flightNumber,

                                          departAirport,

                                          destAirport);

      }

}



public  void  testGetStatus_inital_S()  {

      //  implicit  setup

      //  exercise  SUT  and  verify  outcome

      assertEquals(FlightState.PROPOSED,  flight.getStatus());

      //  teardown

}

public  void  testGetStatus_cancelled()  {

      //  implicit  setup  partially  overridden

      flight.cancel();

      //  exercise  SUT  and  verify  outcome

      assertEquals(FlightState.CANCELLED,  flight.getStatus());

      //  teardown

}

 

根据此处实际设置 Fixture 的代码,它是一个普通的共享 Fixture,但我们也可以在这个激励示例中轻松使用预构建 Fixture(第429页)。无论哪种方式,这些测试都可以随时开始交互。

Based on the code that actually sets up the fixture as shown here, it is a normal Shared Fixture, but we could have just as easily used a Prebuilt Fixture (page 429) for this motivating example. Either way, these tests could start interacting at any time.

重构说明

Refactoring Notes

假设我们正在使用一个共享夹具(相同设计,单一副本)并决定重构它以使用新夹具。我们可以从重构测试开始,以使用新的标准夹具(相同设计,许多副本)。然后,我们可以决定是否要进一步改进测试,以便它通过使用最小化数据(第 738页)重构将夹具设置逻辑修剪到最低限度,从而构建最小夹具第 302页) 。此时也是将需要相同类型测试夹具的测试方法分组到每个夹具的测试用例类并使用隐式设置的好时机;使用标准夹具将减少我们需要设计和构建的最小夹具的数量。

Suppose we are using a Shared Fixture (same design, single copy) and decide to refactor it to use a Fresh Fixture. We can start by refactoring the test to use a fresh Standard Fixture (same design, many copies). Then we can decide whether we want to further evolve the test so that it builds a Minimal Fixture (page 302) by pruning the fixture setup logic to the bare minimum using a Minimize Data (page 738) refactoring. This point would also be good time to group Test Methods that need the same type of test fixture into a Testcase Class per Fixture and use Implicit Setup; this use of a Standard Fixture would reduce the number of Minimal Fixtures we need to design and build.

示例:新鲜装置

Example: Fresh Fixture

以下是将相同的测试转换为Fresh Fixture以避免任何交互测试的可能性:

Here's the same test converted to a Fresh Fixture to avoid any possibility of Interacting Tests:

public void testGetStatus_inital() {

        // 设置

      Flight flight = createAnonymousFlight();

      // 执行 SUT 并验证结果

      assertEquals(FlightState.PROPOSED, flight.getStatus());

      // 拆除

      // 垃圾收集

}



public void testGetStatus_cancelled2() {

      // 设置

      Flight flight = createAnonymousCancelledFlight();

      // 执行 SUT 并验证结果

      assertEquals(FlightState.CANCELLED, flight.getStatus());

      // 拆除

      // 垃圾收集

}

public  void  testGetStatus_inital()  {

        //  setup

      Flight  flight  =  createAnonymousFlight();

      //  exercise  SUT  and  verify  outcome

      assertEquals(FlightState.PROPOSED,  flight.getStatus());

      //  teardown

      //        garbage-collected

}



public  void  testGetStatus_cancelled2()  {

      //  setup

      Flight  flight  =  createAnonymousCancelledFlight();

      //  exercise  SUT  and  verify  outcome

      assertEquals(FlightState.CANCELLED,  flight.getStatus());

      //  teardown

      //        garbage-collected

}

 

请注意使用匿名创建方法(请参阅第415页的创建方法)在每个测试中构造适当的状态对象。Flight

Note the use of Anonymous Creation Methods (see Creation Method on page 415) to construct the appropriate state Flight object in each test.

共享装置

Shared Fixture

也称为

Also known as

共享上下文、剩余 Fixture、重用 Fixture、陈旧 Fixture

Shared Context, Leftover Fixture, Reused Fixture, Stale Fixture

我们如何避免缓慢测试?

How can we avoid Slow Tests?

我们应该使用哪种固定策略?

Which fixture strategy should we use?

我们在许多测试中重复使用测试装置的同一个实例。

We reuse the same instance of the test fixture across many tests.

图像

要执行自动化测试,我们需要一个易于理解且完全确定的文本装置。设置新装置第 311页)可能非常耗时,尤其是当我们处理存储在测试数据库中的复杂系统状态时。

To execute an automated test, we require a text fixture that is well understood and completely deterministic. Setting up a Fresh Fixture (page 311) can be time-consuming, especially when we are dealing with complex system state stored in a test database.

我们可以对多个或多个测试重复使用相同的装置,从而使测试运行得更快。

We can make our tests run faster by reusing the same fixture for several or many tests.

工作原理

How It Works

基本概念非常简单:我们创建一个标准夹具(第305页)夹具,其寿命比单个测试用例对象(第382页)的寿命长。这种方法允许多个测试重用同一个测试夹具,而无需销毁该夹具并在测试之间重新创建它。共享夹具可以是预构建夹具,可在多次测试运行中由一个或多个测试重用,也可以是由一个测试创建并在同一测试运行中由另一个测试重用的夹具。无论哪种情况,关键考虑因素是许多测试不会创建自己的夹具,而是重用其他活动“遗留”的夹具。测试运行速度更快,因为它们需要执行的夹具设置更少,这可能导致测试自动化程序需要做更少的工作来为每个测试定义夹具。

The basic concept is pretty simple: We create a Standard Fixture (page 305) fixture that outlasts the lifetime of a single Testcase Object (page 382). This approach allows multiple tests to reuse the same test fixture without destroying that fixture and recreating it between tests. A Shared Fixture can be either a Prebuilt Fixture that is reused by one or more tests in many test runs or a fixture that is created by one test and reused by another test within the same test run. In either case, the key consideration is that many tests do not create their own fixtures but rather reuse a fixture "left over" from some other activity. The tests run faster because they have less fixture setup to perform, which may result in the test automater having to do less work to define the fixture for each test.

何时使用它

When to Use It

不管我们为何使用它们,共享夹具都会带来一些问题,在开始之前我们应该先了解一下。共享夹具的主要问题是它会导致测试之间产生相互作用,如果一些测试依赖于其他测试的结果,就可能导致不稳定测试(第 228页)。另一个潜在的问题是,一个设计用于服务于许多测试的夹具必然比单个测试所需的最小夹具(第 302页)复杂得多。这种更高的复杂性通常需要花费更多的精力来设计,并且当我们需要修改夹具时,以后可能会导致脆弱夹具(请参阅第239页的脆弱测试)。

Regardless of why we use them, Shared Fixtures come with some baggage that we should understand before we head down this path. The major issue with a Shared Fixture is that it can lead to interactions between tests, possibly resulting in Erratic Tests (page 228) if some tests depend on the outcomes of other tests. Another potential problem is that a fixture designed to serve many tests is bound to be much more complicated than the Minimal Fixture (page 302) needed for a single test. This greater complexity will typically take more effort to design and can lead to a Fragile Fixture (see Fragile Test on page 239) later on down the road when we need to modify the fixture.

共享夹具通常会导致模糊测试第 186页),因为夹具不是在测试内部构建的。可以使用Finder 方法(请参阅第599页的测试实用程序方法)和意图显示名称[SBPP]来访问夹具的相关部分,从而减轻这一潜在缺点。

A Shared Fixture will often result in an Obscure Test (page 186) because the fixture is not constructed inside the test. This potential disadvantage can be mitigated by using Finder Methods (see Test Utility Method on page 599) with Intent-Revealing Names [SBPP] to access the relevant parts of the fixture.

使用共享装置有一些正当理由,也有一些错误理由。许多变体的设计主要是为了减轻使用共享装置带来的负面影响。那么,使用共享装置有哪些好理由呢?

There are some valid reasons for using a Shared Fixture and some misguided ones. Many of the variations have been devised primarily to mitigate the negative consequences of using a Shared Fixture. So, what are good reasons for using a Shared Fixture?

变化:慢速测试

当我们无法承担为每个测试构建新的Fresh Fixture的费用时,我们可以使用Shared Fixture 。通常,这种情况发生在为每个测试构建新 Fixture 需要太多处理时,这通常会导致测试速度变慢第 253页)。这种情况最常发生在我们使用真实测试数据库进行测试时,因为创建每条记录的成本很高。当我们使用 SUT 的 API 创建参考数据时,这种开销的增长往往会加剧,因为 SUT 通常会进行大量输入验证,这可能涉及读取一些刚刚写入的记录。

We can use a Shared Fixture when we cannot afford to build a new Fresh Fixture for each test. Typically, this scenario will occur when it takes too much processing to build a new fixture for each test, which often leads to Slow Tests (page 253). It most commonly occurs when we are testing with real test databases due to the high cost of creating each of the records. This growth in overhead tends to be exacerbated when we use the API of the SUT to create the reference data, because the SUT often does a lot of input validation, which may involve reading some of the just-written records.

更好的解决方案是通过完全不与数据库交互来提高测试运行速度。有关更完整的选项列表,请参阅慢速测试的解决方案和侧栏“不使用共享装置加快测试速度”(第 319页)。

A better solution is to make the tests run faster by not interacting with the database at all. For a more complete list of options, see the solutions to Slow Tests and the sidebar "Faster Tests Without Shared Fixtures" (page 319).


无需共享装置,测试速度更快

对于慢速测试(第 253页)的第一反应通常是切换到共享夹具(第 317页) 方法。然而,还有其他几种解决方案可用。本侧栏描述了几个项目的一些经验。

假数据库

在我们早期的 XP 项目中,我们编写了大量访问数据库的测试。起初我们使用共享装置。然而,当我们遇到交互测试(请参阅第 228页的不稳定测试)和后来的测试运行大战(请参阅不稳定测试)时,我们改用了新鲜装置第 311页)方法。由于这些测试需要相当多的参考数据,因此它们需要很长时间才能运行。平均而言,对于 SUT 对数据库进行的每次读取或写入,每个测试都会执行更多次。运行包含数百个测试的完整测试套件需要 15 分钟,这极大地阻碍了我们快速且频繁地集成工作的能力。

当时,我们使用数据访问层将 SQL 排除在代码之外。我们很快发现,它允许我们用功能等效的假数据库(请参阅第551页的假对象)替换真实数据库。我们首先使用简单的s 来根据键存储对象。这种方法使我们能够“在内存中”而不是针对数据库运行许多较简单的测试。这为我们带来了测试执行时间的显著缩短。HashTable

我们的持久性框架支持对象查询接口。我们能够构建一个针对HashTable数据库实现运行的对象查询解释器,这允许我们的大多数测试完全在内存中运行。平均而言,我们的测试在内存中的运行速度比在数据库中快 50 倍。例如,使用数据库运行需要 10 分钟的测试套件在内存中运行只需 10 秒。

这种方法非常成功,以至于我们在后续的许多项目中都重复使用了相同的测试基础架构。使用伪持久性框架还意味着我们不必费心构建“真实数据库”,直到我们的对象模型稳定下来,而这可能需要几个月的时间。

增量加速

Ted O'Grady 和 Joseph King 是大型极限编程项目(50 多名开发人员、主题专家和测试人员)的敏捷团队负责人。与许多构建以数据库为中心的应用程序的项目团队一样,他们也遭受了测试速度慢的困扰。但他们找到了解决这个问题的方法:截至 2005 年底,他们的签入测试套件在不到 8 分钟的时间内运行完成,而针对数据库的完整测试则需要 8 小时。这是一个相当令人印象深刻的速度差异。以下是他们的故事:

目前,我们定期运行大约 6,700 个测试。我们实际上尝试了一些方法来加快测试速度,这些方法随着时间的推移也在不断发展。2004

年 1 月,我们通过 Toplink 直接针对数据库运行测试。2004

年 6 月,我们修改了应用程序,以便能够针对内存中、进程内 Java 数据库 (HSQL) 运行测试。这将运行时间缩短了一半。2004

年 8 月,我们创建了一个仅用于测试的框架,使 Toplink 无需数据库即可工作。这将运行所有测试的时间缩短了 10 倍。2005

年 7 月,我们构建了一个共享的“签到”测试执行服务器,使我们能够远程运行测试。这最初并没有节省任何时间,但事实证明它非常有用。2005

年 7 月,我们还开始使用集群框架,使我们能够在网络上分布运行测试。这将运行测试的时间缩短了一半。

2005 年 8 月,我们从“签入套件”中删除了 GUI 和主数据(参考数据 crud)测试,只通过 Cruise Control 运行它们。这样将运行时间缩短了大约 15% 到 20%。

 

自 2004 年 5 月以来,我们还让 Cruise Control 定期针对数据库运行所有测试。随着测试数量的增加,Cruise Control 完成 [构建和运行测试] 所需的时间也从 1 小时增加到现在的近 8 小时。

当达到阈值时,开发人员 (a) 在开发过程中频繁运行 [测试] 和 (b) 在人们等待令牌签到时创建很长的签到队列,我们​​会通过尝试新技术来适应。通常,我们会尝试将测试运行时间保持在 5 分钟以内,超过 8 分钟就会触发尝试新事物。

到目前为止,我们抵制了只运行部分测试的诱惑,而是专注于加快运行所有测试的方法——尽管如您所见,我们已经开始删除开发人员必须连续运行的测试(例如,主数据和 GUI 测试套件不需要签到,因为它们由 Cruise Control 运行,并且是很少更改的区域)。

最近最有趣的两个解决方案(除了内存框架之外)是测试服务器和集群框架。

 

测试服务器(此处称为“签到”框)实际上非常有用,并且已被证明是可靠和强大的。我们购买了 Opteron 框,它的速度大约是开发框的两倍(真的,这是我们能找到的最快的框)。服务器为维修站中的每台开发机器设置了一个帐户。使用 UNIX 工具 rsynch,Eclipse 工作区与用户相应的服务器帐户文件系统同步。然后,一系列 shell 脚本在服务器上为远程帐户重新创建数据库并运行所有开发测试。测试完成后,运行每个测试的时间列表将转储到控制台,同时转储一个包含所有测试失败的 MyTestSuite.java 类,开发人员可以使用它在本地运行以修复任何已中断的测试。远程服务器提供的最大优势是它使运行大量测试再次感觉很快,因为开发人员可以在等待测试服务器的结果返回时继续工作。

集群框架(基于 Condor)速度很快,但有一个缺陷,即它必须将整个工作区(11MB)发送到网络中的所有节点(×20),这会产生很大的成本,尤其是当十几个对使用它时。相比之下,测试服务器使用 rsynch,它只复制开发人员工作区中的新文件或不同文件。事实证明,集群框架的可靠性也低于服务器解决方案,经常不返回任何测试运行状态。还有一些测试无法在框架上可靠运行。由于它给我们的性能与“签入”测试服务器大致相同,因此我们将此解决方案搁置一旁。

 

进一步阅读

关于第一次体验的更详细描述可以在http://FasterTestsPaper.gerardmeszaros.com找到。



Faster Tests Without Shared Fixtures

The first reaction to Slow Tests (page 253) is often to switch to a Shared Fixture (page 317) approach. Several other solutions are available, however. This sidebar describes some experiences on several projects.

Fake Database

On one of our early XP projects, we wrote a lot of tests that accessed the database. At first we used a Shared Fixture. When we encountered Interacting Tests (see Erratic Test on page 228) and later Test Run Wars (see Erratic Test), however, we changed to a Fresh Fixture (page 311) approach. Because these tests needed a fair bit of reference data, they were taking a long time to run. On average, for every read or write the SUT did to or from the database, each test did several more. It was taking 15 minutes to run the full test suite of several hundred tests, which greatly impeded our ability to integrate our work quickly and often.

At the time, we were using a data access layer to keep the SQL out of our code. We soon discovered that it allowed us to replace the real database with a functionally equivalent Fake Database (see Fake Object on page 551). We started out by using simple HashTables to store the objects against a key. This approach allowed us to run many of our simpler tests "in memory" rather than against the database. And that bought us a significant drop in test execution time.

Our persistence framework supported an object query interface. We were able to build an interpreter of the object queries that ran against our HashTable database implementation and that allowed the majority of our tests to work entirely in memory. On average, our tests ran about 50 times faster in memory than with the database. For example, a test suite that took 10 minutes to run with the database took 10 seconds to run in memory.

This approach was so successful that we have reused the same testing infrastructure on many of our subsequent projects. Using the faked-out persistence framework also means we don't have to bother with building a "real database" until our object models stabilize, which can be several months into the project.

Incremental Speedups

Ted O'Grady and Joseph King are agile team leads on a large (50-plus developers, subject matter experts, and testers) eXtreme Programming project. Like many project teams building database-centric applications, they suffered from Slow Tests. But they found a way around this problem: As of late 2005, their check-in test suite ran in less than 8 minutes compared to 8 hours for a full test run against the database. That is a pretty impressive speed difference. Here is their story:

Currently we have about 6,700 tests that we run on a regular basis. We've actually tried a few things to speed up the tests and they've evolved over time.

In January 2004, we were running our tests directly against a database via Toplink.

In June 2004, we modified the application so we could run tests against an in-memory, in-process Java database (HSQL). This cut the time to run in half.

In August 2004, we created a test-only framework that allowed Toplink to work without a database at all. That cut the time to run all the tests by a factor of 10.

In July 2005, we built a shared "check-in" test execution server that allowed us to run tests remotely. This didn't save any time at first but it has proven to be quite useful nonetheless.

In July 2005, we also started using a clustering framework that allowed us to run tests distributed across a network. This cut the time to run the tests in half.

In August 2005, we removed the GUI and Master Data (reference data crud) tests from the "check-in suite" and ran them only from Cruise Control. This cut the time to run by approximately 15% to 20%.

 

Since May 2004, we have also had Cruise Control run all the tests against the database at regular intervals. The time it takes Cruise Control to complete [the build and run the tests] has grown with the number of tests from an hour to nearly 8 hours now.

When a threshold has been met that prevents the developers from (a) running [the tests] frequently when developing and (b) creating long check-in queues as people wait for the token to check in, we have adapted by experimenting with new techniques. As a rule we try to keep the running of the tests under 5 minutes, with anything over 8 minutes being a trigger to try something new.

We have resisted thus far the temptation to run only a subset of the tests and instead focused on ways to speed up running all the tests—although as you can see, we have begun removing the tests developers must run continuously (e.g., Master Data and GUI test suites are not required to check in, as they are run by Cruise Control and are areas that change infrequently).

Two of the most interesting solutions recently (aside from the in-memory framework) are the test server and the clustering framework.

 

The test server (named the "check-in" box here) is actually quite useful and has proven to be reliable and robust. We bought an Opteron box that is roughly twice as fast as the development boxes (really, the fastest box we could find). The server has an account set up for each development machine in the pit. Using the UNIX tool rsynch, the Eclipse workspace is synchronized with the user's corresponding server account file system. A series of shell scripts then recreates the database on the server for the remote account and runs all the development tests. When the tests have completed, a list of times to run each test is dumped to the console, along with a MyTestSuite.java class containing all the test failures, which the developer can use to run locally to fix any tests that have broken. The biggest advantage the remote server has provided is that it makes running a large number of tests feel fast again, because the developer can continue working while he or she waits for the results of the test server to come back.

The clustering framework (based on Condor) was quite fast but had the defect that it had to ship the entire workspace (11MB) to all the nodes on the network (×20), which had a significant cost, especially when a dozen pairs are using it. In comparison, the test server uses rsynch, which copies only the files that are new or different in the developer's workspace. The clustering framework also proved to be less reliable than the server solution, frequently not returning any status of the test run. There were also some tests that would not run reliably on the framework. Since it gave us roughly the same performance as the "check-in" test server, we have put this solution on the back burner.

 

Further Reading

A more detailed description of the first experience can be found at http://FasterTestsPaper.gerardmeszaros.com.


 
变化:增量测试

当我们有一个长而复杂的操作序列,每个操作都依赖于之前的操作时,我们也可以使用共享装置。在客户测试中,这可能显示为工作流;在单元测试中,它可能是同一对象上的一系列方法调用。这种情况可以使用单个Eager 测试进行测试(请参阅第 224页上的断言轮盘赌)。另一种方法是将每个不同的操作放入单独的测试方法第 348页),该方法基于在共享装置上运行的先前测试的操作。这种方法是链式测试第 454页)的一个例子,也是“测试”(即 QA)社区中的测试人员经常使用的方法:他们设置一个装置,然后运行一系列测试,每个测试都基于该装置构建。与我们的全自动测试(见第26页)相比,测试人员确实有一个显著的优势:当链中途的测试失败时,他们可以决定如何恢复或是否值得继续进行。相比之下,我们的自动化测试只是继续运行,其中许多测试会产生测试失败或错误,因为它们没有按预期找到装置,因此 SUT 的行为(可能正确)不同。由此产生的测试结果可能会掩盖失败的真正原因。凭借一些经验,通常可以识别故障模式并推断出根本原因。10

We may also use Shared Fixtures when we have a long, complex sequence of actions, each of which depends on the previous actions. In customer tests, this may show up as a work flow; in unit tests, it may be a sequence of method calls on the same object. This case might be tested using a single Eager Test (see Assertion Roulette on page 224). The alternative is to put each distinct action into a separate Test Method (page 348) that builds upon the actions of a previous test operating on a Shared Fixture. This approach, which is an example of Chained Tests (page 454), is how testers in the "testing" (i.e., QA) community often operate: They set up a fixture and then run a sequence of tests, each of which builds upon the fixture. The testers do have one significant advantage over our Fully Automated Tests (see page 26): When a test partway through the chain fails, they are available to make decisions about how to recover or whether it is worth proceeding at all. In contrast, our automated tests just keep running, and many of them will generate test failures or errors because they did not find the fixture as expected and, therefore, the SUT behaved (probably correctly) differently. The resulting test results can obscure the real cause of the failure in a sea of red. With some experience it is often possible to recognize the failure pattern and deduce the root cause.10

通过在开始每个测试方法时使用一个或多个Guard Assertion第 490页)记录测试方法对装置状态的假设,可以简化此故障排除。当这些断言失败时,它们会告诉我们去其他地方查找 - 要么查看测试套件中之前失败的测试,要么查看测试运行的顺序。

This troubleshooting can be made simpler by starting each Test Method with one or more Guard Assertions (page 490) that document the assumptions the Test Method makes about the state of the fixture. When these assertions fail, they tell us to look elsewhere—either at tests that failed earlier in the test suite or at the order in which the tests were run.

实施说明

Implementation Notes

共享夹具的一个关键实现问题是,测试如何了解共享夹具中的对象,以便它们可以(重新)使用它们?由于共享夹具的目的是通过让多个测试使用同一个测试夹具实例来节省执行时间和精力,因此我们需要保留对我们创建的夹具的引用。这样,如果夹具已经存在,我们就可以找到它,并且我们可以在构造夹具后通知其他测试它现在存在。使用每次运行夹具时,我们可以有更多选择,因为我们可以比由其他程序设置的预构建夹具第 429页)更容易“记住”我们在代码中设置的夹具。虽然我们可以将夹具对象的标识符(例如数据库键)硬编码到所有测试中,但这种技术会导致脆弱的夹具。为了避免这个问题,我们需要在创建夹具时保留对夹具的引用,并且我们需要让所有测试都可以访问该引用。

A key implementation question with Shared Fixtures is, How do tests know about the objects in the Shared Fixture so they can (re)use them? Because the point of a Shared Fixture is to save execution time and effort by having multiple tests use the same instance of the test fixture, we'll need to keep a reference to the fixture we create. That way, we can find the fixture if it already exists and we can inform other tests that it now exists once we have constructed it. We have more choices available to us with Per-Run Fixtures because we can "remember" the fixture we set up in code more easily than a Prebuilt Fixture (page 429) set up by a different program. Although we could just hard-code the identifiers (e.g., database keys) of the fixture objects into all our tests, that technique would result in a Fragile Fixture. To avoid this problem, we need to keep a reference to the fixture when we create it and we need to make it possible for all tests to access that reference.

变化:每次运行固定装置

共享 Fixture的最简单形式是Per-Run Fixture,我们在测试运行开始时设置 Fixture,并允许它由运行中的测试共享。理想情况下,Fixture 不会比测试运行更长寿,我们不必担心测试运行之间的交互,例如不可重复的测试(不稳定测试的原因)。如果 Fixture持久性的,例如当它存储在数据库中时,我们可能需要进行显式 Fixture 拆卸。

The simplest form of Shared Fixture is the Per-Run Fixture, in which we set up the fixture at the beginning of a test run and allow it to be shared by the tests within the run. Ideally, the fixture won't outlive the test run and we don't have to worry about interactions between test runs such as Unrepeatable Tests (a cause of Erratic Tests). If the fixture is persistent, such as when it is stored in a database, we may need to do explicit fixture teardown.

如果每次运行装置仅在单个测试用例类(第 373页) 内共享,则最简单的解决方案是使用类变量来保存我们需要保存引用的每个装置对象,然后使用延迟设置(第435页) 或套件装置设置(第441页) 在运行套件中的第一个测试之前初始化对象。如果我们想在多个测试用例类之间共享测试装置,我们需要使用设置装饰器(第 447页) 来保存setUptearDown方法,并使用测试装置注册表(参见测试助手(第643页) )(可能只是测试数据库) 来访问装置。

If a Per-Run Fixture is shared only within a single Testcase Class (page 373), the simplest solution is to use a class variable for each fixture object we need to hold a reference to and then use either Lazy Setup (page 435) or Suite Fixture Setup (page 441) to initialize the objects just before we run the first test in the suite. If we want to share the test fixture between many Testcase Classes, we'll need to use a Setup Decorator (page 447) to hold the setUp and tearDown methods and a Test Fixture Registry (see Test Helper on page 643) (which could just be the test database) to access the fixture.

变体:不可变共享装置

共享夹具的问题在于,如果测试修改了共享夹具第 317页) ,就会导致不稳定的测试共享夹具违反了独立测试原则(见第42页)。我们可以通过使共享夹具不可变来避免这个问题;也就是说,我们将测试所需的夹具分成两个逻辑部分。第一部分是每个测试都需要存在但永远不会被任何测试修改的东西——即不可变的共享夹具。第二部分是任何测试都需要修改或删除的对象;这些对象应该由每个测试作为新鲜的夹具构建。

The problem with Shared Fixtures is that they lead to Erratic Tests if tests modify the Shared Fixture (page 317). Shared Fixtures violate the Independent Test principle (see page 42). We can avoid this problem by making the Shared Fixture immutable; that is, we partition the fixture needed by tests into two logical parts. The first part is the stuff every test needs to have present but is never modified by any tests—that is, the Immutable Shared Fixture. The second part is the objects that any test needs to modify or delete; these objects should be built by each test as Fresh Fixtures.

应用不可变共享夹具最困难的部分是决定什么构成对对象的更改。关键准则是:如果任何测试将另一测试所做的事情视为对不可变共享夹具中对象的更改,则不应允许在与其共享夹具的任何测试中进行该更改。最常见的是,不可变共享夹具由实际每个测试夹具所需的参考数据组成。然后可以在不可变共享夹具之上将每个测试夹具构建为新夹具

The most difficult part of applying an Immutable Shared Fixture is deciding what constitutes a change to an object. The key guideline is this: If any test perceives something done by another test as a change to an object in the Immutable Shared Fixture, then that change shouldn't be allowed in any test with which it shares the fixture. Most commonly, the Immutable Shared Fixture consists of reference data that is needed by the actual per-test fixtures. The per-test fixtures can then be built as Fresh Fixtures on top of the Immutable Shared Fixture.

激励人心的例子

Motivating Example

以下示例展示了通过隐式设置第 424页)设置测试夹具的测试用例类。每个测试方法都使用一个实例变量来访问夹具的内容。

The following example shows a Testcase Class setting up the test fixture via Implicit Setup (page 424). Each Test Method uses an instance variable to access the contents of the fixture.

public void testGetFlightsByFromAirport_OneOutboundFlight()

                  throws Exception {

      setupStandardAirportsAndFlights();

      FlightDto outboundFlight = findOneOutboundFlight();

      // 练习系统

      列表 flightsAtOrigin = Facade.getFlightsByOriginAirport(

                                outboundFlight.getOriginAirportId());

      // 验证结果

      assertOnly1FlightInDtoList( "Flights at origin",

                                                 outboundFlight,

                                                 flightsAtOrigin);

}



public void testGetFlightsByFromAirport_TwoOutboundFlights()

                  throws Exception {

      setupStandardAirportsAndFlights();

      FlightDto[] outboundFlights =

                          findTwoOutboundFlightsFromOneAirport();

      // 练习系统

      列表 flightsAtOrigin = Facade.getFlightsByOriginAirport(

                                      outboundFlights[0].getOriginAirportId());

      // 验证结果

      assertExactly2FlightsInDtoList( "出发地航班",

                                                       outboundFlights,

                                                       flightsAtOrigin);

}

public  void  testGetFlightsByFromAirport_OneOutboundFlight()

                  throws  Exception  {

      setupStandardAirportsAndFlights();

      FlightDto  outboundFlight  =  findOneOutboundFlight();

      //  Exercise  System

      List  flightsAtOrigin  =  facade.getFlightsByOriginAirport(

                                outboundFlight.getOriginAirportId());

      //  Verify  Outcome

      assertOnly1FlightInDtoList(  "Flights  at  origin",

                                                 outboundFlight,

                                                 flightsAtOrigin);

}



public  void  testGetFlightsByFromAirport_TwoOutboundFlights()

                  throws  Exception  {

      setupStandardAirportsAndFlights();

      FlightDto[]  outboundFlights  =

                          findTwoOutboundFlightsFromOneAirport();

      //  Exercise  System

      List  flightsAtOrigin  =  facade.getFlightsByOriginAirport(

                                      outboundFlights[0].getOriginAirportId());

      //  Verify  Outcome

      assertExactly2FlightsInDtoList(  "Flights  at  origin",

                                                       outboundFlights,

                                                       flightsAtOrigin);

}

 

请注意,该方法对每个测试方法setUp运行一次。如果装置设置相当复杂并涉及访问数据库,则此方法可能会导致测试速度变慢

Note that the setUp method is run once for each Test Method. If the fixture setup is fairly complex and involves accessing a database, this approach could result in Slow Tests.

重构说明

Refactoring Notes

要将Testcase ClassStandard Fixture转换为Shared Fixture,我们只需将实例变量转换为类变量,以使 Fixture 比创建的Testcase Object更持久。然后,我们只需初始化一次类变量,以避免为每个Test Method重新创建它们;Lazy Setup是完成此任务的一种简单方法。当然,也可以使用其他方法来设置Shared Fixture,例如Setup DecoratorSuite Fixture Setup

To convert a Testcase Class from a Standard Fixture to a Shared Fixture, we simply convert the instance variables into class variables to make the fixture outlast the creating Testcase Object. We then need to initialize the class variables just once to avoid recreating them for each Test Method; Lazy Setup is an easy way to accomplish this task. Of course, other ways to set up the Shared Fixture are also possible, such as Setup Decorator or Suite Fixture Setup.

示例:共享装置

Example: Shared Fixture

此示例显示了使用Lazy Setup将灯具转换为共享灯具设置。

This example shows the fixture converted to a Shared Fixture set up using Lazy Setup.

protected void setUp() throws Exception {

      if (sharedFixtureInitialized) {

          return;

          }

          façade = new FlightMgmtFacadeImpl();

          setupStandardAirportsAndFlights();

          sharedFixtureInitialized = true;

}



protected void teaDown() throws Exception {

          // 我们不能删除任何对象,因为我们不知道

          // 这是否是最后一次测试

}

protected  void  setUp()  throws  Exception  {

      if  (sharedFixtureInitialized)  {

          return;

          }

          facade  =  new  FlightMgmtFacadeImpl();

          setupStandardAirportsAndFlights();

          sharedFixtureInitialized  =  true;

}



protected  void  tearDown()  throws  Exception  {

          //  We  cannot  delete  any  objects  because  we  don't  know

          //  whether  this  is  the  last  test

}

 

方法中的延迟初始化[SBPP]setUp逻辑可确保每当类变量未初始化时都会创建共享装置。测试方法也已修改为使用Finder 方法来访问装置的内容:

The Lazy Initialization [SBPP] logic in the setUp method ensures that the Shared Fixture is created whenever the class variable is uninitialized. The Test Methods have also been modified to use a Finder Method to access the contents of the fixture:

public void testGetFlightsByFromAirport_OneOutboundFlight()

               throws Exception {

      FlightDto outboundFlight = findOneOutboundFlight();

      // 练习系统

      列表 flightsAtOrigin = Facade.getFlightsByOriginAirport(

                                       outboundFlight.getOriginAirportId());

      // 验证结果

      assertOnly1FlightInDtoList( "Flights at origin",

                                                 outboundFlight,

                                                 flightsAtOrigin);

}



public void testGetFlightsByFromAirport_TwoOutboundFlights()

               throws Exception {

      FlightDto[] outboundFlights =

            findTwoOutboundFlightsFromOneAirport();

      // 练习系统

      列表 flightsAtOrigin = facade.getFlightsByOriginAirport(

                              outboundFlights[0].getOriginAirportId());

      // 验证结果

      assertExactly2FlightsInDtoList( "Flights at origin",

                                                       outboundFlights,

                                                       flightsAtOrigin);

}

public  void  testGetFlightsByFromAirport_OneOutboundFlight()

               throws  Exception  {

      FlightDto  outboundFlight  =  findOneOutboundFlight();

      //  Exercise  System

      List  flightsAtOrigin  =  facade.getFlightsByOriginAirport(

                                       outboundFlight.getOriginAirportId());

      //  Verify  Outcome

      assertOnly1FlightInDtoList(  "Flights  at  origin",

                                                 outboundFlight,

                                                 flightsAtOrigin);

}



public  void  testGetFlightsByFromAirport_TwoOutboundFlights()

               throws  Exception  {

      FlightDto[]  outboundFlights  =

            findTwoOutboundFlightsFromOneAirport();

      //  Exercise  System

      List  flightsAtOrigin  =  facade.getFlightsByOriginAirport(

                              outboundFlights[0].getOriginAirportId());

      //  Verify  Outcome

      assertExactly2FlightsInDtoList(  "Flights  at  origin",

                                                       outboundFlights,

                                                       flightsAtOrigin);

}

 

示例:不可变共享装置

Example: Immutable Shared Fixture

以下是共享装置“污染”的一个例子:

Here's an example of Shared Fixture "pollution":

public void testCancel_proposed_p()throws Exception {

    // 共享夹具

    BigDecimalposedFlightId = findProposedFlight();

    // 练习 SUT

    Facade.cancelFlight(proposedFlightId);

    // 验证结果

    try{

        assertEquals(FlightState.CANCELLED,

                            Facade.findFlightById(proposedFlightId));

    } finally {

         // 拆卸

         // 尝试消除损害;希望这有效!

         facade.overrideStatus(posedFlightId,

                                            FlightState.PROPOSED);

    }

}

public  void  testCancel_proposed_p()throws  Exception  {

    //  shared  fixture

    BigDecimal  proposedFlightId  =  findProposedFlight();

    //            exercise  SUT

    facade.cancelFlight(proposedFlightId);

    //  verify  outcome

    try{

        assertEquals(FlightState.CANCELLED,

                            facade.findFlightById(proposedFlightId));

    }  finally  {

         //  teardown

         //  try  to  undo  the  damage;  hope  this  works!

         facade.overrideStatus(  proposedFlightId,

                                            FlightState.PROPOSED);

    }

}

 

我们可以通过使共享夹具不可变来避免这个问题;也就是说,我们将测试所需的夹具分成两个逻辑部分。第一部分是每个测试都需要存在但永远不会被任何测试修改的内容 - 即不可变的共享夹具。第二部分是任何测试都需要修改或删除的对象;这些对象应该由每个测试作为新鲜夹具构建。

We can avoid this problem by making the Shared Fixture immutable; that is, we partition the fixture needed by tests into two logical parts. The first part is the stuff every test needs to have present but is never modified by any tests—that is, the Immutable Shared Fixture. The second part is the objects that any test needs to modify or delete; these objects should be built by each test as Fresh Fixtures.

这是经过修改以使用Immutable Shared Fixture 的相同测试。我们只是mutableFlight在测试中创建了自己的测试。

Here's the same test modified to use an Immutable Shared Fixture. We simply created our own mutableFlight within the test.

public void testCancel_proposed() throws Exception {

    // 固定设置

    BigDecimal mutableFlightId =

          createFlightBetweenInsigificantAirports();

    // 练习 SUT

    Facade.cancelFlight(mutableFlightId);

    // 验证结果

    assertEquals( FlightState.CANCELLED,

                         Facade.findFlightById(mutableFlightId));

    // 拆卸

    // 无需拆卸,因为我们让 SUT

    为每个航班创建 // 新 ID。我们最终可能需要清理

    // 数据库。

}

public  void  testCancel_proposed()  throws  Exception  {

    //  fixture  setup

    BigDecimal  mutableFlightId  =

          createFlightBetweenInsigificantAirports();

    //  exercise  SUT

    facade.cancelFlight(mutableFlightId);

    //  verify  outcome

    assertEquals(  FlightState.CANCELLED,

                         facade.findFlightById(mutableFlightId));

    //  teardown

    //     None  required  because  we  let  the  SUT  create

    //     new  IDs  for  each  flight.  We  might  need  to  clean  out

    //     the  database  eventually.

}

 

请注意,我们不需要此版本的测试中的任何装置拆卸逻辑,因为 SUT 使用不同的生成值(请参阅第723页的生成值)— 也就是说,我们不提供航班号。我们还使用预定义的和,以避免更改其他测试使用的机场的航班数量。因此,可变航班可以毫无问题地累积在数据库中。dummyAirport1dummyAirport2

Note that we don't need any fixture teardown logic in this version of the test because the SUT uses a Distinct Generated Value (see Generated Value on page 723)—that is, we do not supply a flight number. We also use the predefined dummyAirport1 and dummyAirport2 to avoid changing the number of flights for airports used by other tests. Therefore, the mutable flights can accumulate in the database trouble-free.

后门操纵

Back Door Manipulation

也称为

Also known as

跨层测试

Layer-Crossing Test

当我们无法使用往返测试时,我们如何独立验证逻辑?

How can we verify logic independently when we cannot use a round-trip test?

我们通过后门(例如直接数据库访问)来设置测试装置或验证结果。

We set up the test fixture or verify the outcome by going through a back door (such as direct database access).

图像

每个测试都需要一个起点(测试装置)和一个预期的终点(预期结果)。“正常”方法是设置装置并使用 SUT 本身的 API 验证结果。在某些情况下,这是不可能的或不理想的。

Every test requires a starting point (the test fixture) and an expected finishing point (the expected results). The "normal" approach is to set up the fixture and verify the outcome by using the API of the SUT itself. In some circumstances this is either not possible or not desirable.

在某些情况下,我们可以使用后门操作来设置装置和/或验证 SUT 的状态。

In some situations we can use Back Door Manipulation to set up the fixture and/or verify the SUT's state.

工作原理

How It Works

SUT 的状态有多种形式。它可以存储在内存中、磁盘上的文件、数据库中或与 SUT 交互的其他应用程序中。无论采用何种形式,测试的先决条件通常要求 SUT 的状态不仅是已知的,而且是特定的状态。同样,在测试结束时,我们通常希望对 SUT 的状态进行状态验证(第462页)。

The state of the SUT comes in many flavors. It can be stored in memory, on disk as files, in a database, or in other applications with which the SUT interacts. Whatever form it takes, the pre-conditions of a test typically require that the state of the SUT is not just known but is a specific state. Likewise, at the end of the test we often want to do State Verification (page 462) of the SUT's state.

如果我们可以从 SUT 外部访问 SUT 的状态,则测试可以通过绕过 SUT 的常规 API 并通过“后门”直接与保持该状态的任何东西交互来设置 SUT 的测试前状态。当 SUT 的执行完成后,测试可以类似地通过后门访问 SUT 的测试后状态,以将其与预期结果进行比较。对于客户测试,后门最常见的是测试数据库,但也可能是 SUT 所依赖的其他组件,包括注册表[PEAA]对象甚至文件系统。对于单元测试,后门是其他类或对象或 SUT 的替代接口(或测试特定子类;第579页),它以“普通”客户端不会使用的方式公开状态。如果这会使工作更容易,我们还可以用适当配置的测试替身第 522页)替换依赖组件 (DOC),而不是使用真实组件。

If we have access to the state of the SUT from outside the SUT, the test can set up the pre-test state of the SUT by bypassing the normal API of the SUT and interacting directly with whatever is holding that state via a "back door." When exercising of the SUT has been completed, the test can similarly access the post-test state of the SUT via a back door to compare it with expected outcome. For customer tests, the back door is most commonly a test database, but it could also be some other component on which the SUT depends, including a Registry [PEAA] object or even the file system. For unit tests, the back door is some other class or object or an alternative interface of the SUT (or a Test-Specific Subclass; page 579) that exposes the state in a way "normal" clients wouldn't use. We can also replace a depended-on component (DOC) with a suitably configured Test Double (page 522) instead of using the real thing if that makes the job easier.

何时使用它

When to Use It

我们可能出于多种原因选择使用后门操作,稍后我们将更详细地讨论这些原因。使用此技术的先决条件是必须存在某种通向系统状态的后门。后门操作的主要缺点是我们的测试——或者它们调用的测试实用程序方法第 599页)——与我们关于如何表示 SUT 状态的设计决策更加紧密地耦合在一起。如果我们需要更改这些决策,我们可能会遇到脆弱测试第 239页)。我们需要根据具体情况来决定这个代价是否可以接受。通过将所有后门操作封装测试实用程序方法中,我们可以大大减少紧密耦合的影响。

We might choose to use Back Door Manipulation for several reasons which we'll examine in more detail shortly. A prerequisite for using this technique is that some sort of back door to the state of the system must exist. The main drawback of Back Door Manipulation is that our tests—or the Test Utility Methods (page 599) they call—become much more closely coupled to the design decisions we make about how to represent the state of the SUT. If we need to change those decisions, we may encounter Fragile Tests (page 239). We need to decide whether this price is acceptable on a case-by-case basis. We can greatly reduce the impact of the close coupling by encapsulating all Back Door Manipulation in Test Utility Methods.

使用后门操作还可能通过隐藏测试结果与测试装置的关系而导致模糊测试第 186页)。我们可以通过将传递给后门操作机制的测试数据包含在测试用例类第 373页)中来避免此问题,或者至少通过使用Finder 方法(请参阅测试实用程序方法)通过意图揭示名称引用装置中的对象来缓解此问题。

Using Back Door Manipulation can also lead to Obscure Tests (page 186) by hiding the relationship of the test outcome to the test fixture. We can avoid this problem by including the test data being passed to the Back Door Manipulation mechanism within the Testcase Class (page 373), or at least mitigate it by using Finder Methods (see Test Utility Method) to refer to the objects in the fixture via intent-revealing names.

后门操纵的常见应用涉及测试 SUT 状态的基本 CRUD(创建、读取、更新、删除)操作。在这种情况下,我们希望验证信息是否持久化并且可以以相同的形式恢复。如果不测试“创建”,就很难为“读取”编写往返测试;同样,如果不测试“创建”和“读取”,就很难测试“更新”或“删除”。我们当然可以使用往返测试来测试这些操作,但这种测试不会检测到某些类型的系统问题,例如将信息放入错误的数据库列中。一种解决方案是进行跨层测试,使用后门操纵直接设置或验证数据库的内容。对于“读取”测试,测试使用后门设置设置数据库的内容,然后要求 SUT 读取数据。对于“写入”测试,测试要求系统写入某些对象,然后对数据库的内容使用后门验证。

A common application of Back Door Manipulation involves testing basic CRUD (Create, Read, Update, Delete) operations on the SUT's state. In such a case, we want to verify that the information persisted and can be recovered in the same form. It is difficult to write round-trip tests for "Read" without also testing "Create"; likewise, it is difficult to test "Update" or "Delete" without testing both "Create" and "Read." We can certainly test these operations by using round-trip tests, but this kind of testing won't detect certain types of systemic problems, such as putting information into the wrong database column. One solution is to conduct layer-crossing tests that use Back Door Manipulation to set up or verify the contents of the database directly. For a "Read" test, the test sets up the contents of the database using Back Door Setup and then asks the SUT to read the data. For a "Write" test, the test asks the system to write certain objects and then uses Back Door Verification on the contents of the database.

变体:后门设置

进行后门操作的一个原因是使测试运行得更快。如果系统在将数据放入其数据存储之前进行了大量处理,则测试通过 SUT 的 API 设置装置所需的时间可能相当长。使测试运行得更快的一种方法是确定这些数据存储应该是什么样子,然后创建一种通过后门而不是通过 API 设置它们的方法。不幸的是,这种技术引入了它自己的问题:由于后门设置绕过了对象创建业务规则的执行,我们可能会发现自己创建的装置不切实际,甚至可能无效。随着业务规则根据不断变化的业务需求进行修改,这个问题可能会随着时间的推移而逐渐显现。同时,这种方法可能允许我们创建 SUT 不允许我们通过其 API 设置的测试场景。

One reason for doing Back Door Manipulation is to make tests run faster. If a system does a lot of processing before putting data into its data store, the time it takes for a test to set up the fixture via the SUT's API could be quite significant. One way to make the tests run faster is to determine what those data stores should look like and then create a means to set them up via the back door rather than through the API. Unfortunately, this technique introduces its own problem: Because Back Door Setup bypasses enforcement of the object creation business rules, we may find ourselves creating fixtures that are not realistic and possibly even invalid. This problem may creep in over time as the business rules are modified in response to changing business needs. At the same time, this approach may allow us to create test scenarios that the SUT will not let us set up through its API.

当我们在 SUT 和另一个应用程序之间共享数据库时,我们需要验证我们是否正确使用了数据库,以及我们是否可以处理其他应用程序可能创建的所有可能的数据配置。后门设置是建立这些配置的好方法 - 如果 SUT 不写入这些表或仅写入特定(且有效)的数据配置,它可能是唯一的方法。后门设置让我们可以轻松创建这些“不可能”的配置,以便我们可以验证 SUT 在这些情况下的行为方式。

When we share a database between our SUT and another application, we need to verify that we are using the database correctly and that we can handle all possible data configurations the other applications might create. Back Door Setup is a good way to establish these configurations—and it may be the only way if the SUT either doesn't write those tables or writes only specific (and valid) data configurations. Back Door Setup lets us create those "impossible" configurations easily so we can verify how the SUT behaves in these situations.

变体:后门验证

后门验证涉及通过后门潜入对 SUT 的运行后状态进行状态验证;它主要适用于客户测试(或有时称为功能测试)。后门通常是检查数据库中对象的另一种方法,通常通过标准 API(例如 SQL)或通过数据导出,然后可以使用文件比较实用程序进行检查。

Back Door Verification involves sneaking in to do State Verification of the SUT's post-exercise state via a back door; it is mostly applicable to customer tests (or functional tests, as they are sometimes called). The back door is typically an alternative way to examine the objects in the database, usually through a standard API such as SQL or via data exports that can then be examined with a file comparison utility program.

如前所述,后门操作可以使测试运行得更快。如果获取 SUT 状态的唯一方法是调用昂贵的操作(例如复杂的报告)或进一步修改 SUT 状态的操作,我们最好使用后门操作

As mentioned earlier, Back Door Manipulation can make tests run faster. If the only way to get at the SUT's state is to invoke an expensive operation (such as a complex report) or an operation that further modifies the SUT's state, we may be better off using Back Door Manipulation.

进行后门操作的另一个原因是,其他系统希望 SUT 以特定方式存储其状态,然后它们可以直接访问。这是一种间接输出形式。在这种情况下,标准往返测试无法证明 SUT 的行为是正确的,因为如果“写入”和“读取”操作犯了相同的错误(例如将信息放入错误的数据库列中),它们就无法检测到系统问题。解决方案是进行跨层测试,直接查看数据库的内容以验证信息是否存储正确。对于“写入”测试,测试要求系统写入某些对象,然后通过后门检查数据库的内容。

Another reason for doing Back Door Manipulation is that other systems expect the SUT to store its state in a specific way, which they can then access directly. This is a form of indirect output. In this situation, standard round-trip tests cannot prove that the SUT's behavior is correct because they cannot detect a systematic problem if the "Write" and "Read" operations make the same mistake, such as putting information into the wrong database column. The solution is a layer-crossing test that looks at the contents of the database directly to verify that the information is stored correctly. For a "Write" test, the test asks the system to write certain objects and then inspects the contents of the database via the back door.

变体:后门拆卸

我们还可以使用后门操作来拆除存储在测试数据库中的Fresh Fixture (第 311页)。如果我们可以使用批量数据库命令清除整个表,例如表截断拆除(第 661页) 或事务回滚拆除(第 668页),则此功能尤其有用。

We can also use Back Door Manipulation to tear down a Fresh Fixture (page 311) that is stored in a test database. This ability is especially beneficial if we can use bulk database commands to wipe clean whole tables, as in Table Truncation Teardown (page 661) or Transaction Rollback Teardown (page 668).

实施说明

Implementation Notes

我们如何实现后门操作取决于 Fixture 的位置以及我们访问 SUT 状态的难易程度。这还取决于我们为什么要进行后门操作。本节列出了最常见的实现,但您可以自由发挥想象力,想出使用此模式的其他方法。

How we implement Back Door Manipulation depends on where the fixture lives and how easily we can access the state of the SUT. It also depends on why we are doing Back Door Manipulation. This section lists the most common implementations, but feel free to use your imagination and come up with other ways to use this pattern.

变体:数据库填充脚本

当 SUT 将其状态存储在运行时访问的数据库中时,执行后门操作的最简单方法是在调用 SUT 之前将数据直接加载到该数据库中。这种方法在我们编写客户测试时最常见,但如果我们正在测试的类直接与数据库交互,则单元测试也可能需要这种方法。我们必须首先确定测试的先决条件,并根据该信息确定测试所需的数据。然后,我们定义一个数据库脚本,该脚本绕过 SUT 逻辑将相应的记录直接插入数据库。每当我们想要设置测试装置时,我们都会使用此数据库填充脚本- 这一决定取决于我们选择的测试装置策略。(有关该主题的更多信息,请参阅第 6 章测试自动化策略”。)

When the SUT stores its state in a database that it accesses as it runs, the easiest way to do Back Door Manipulation is to load data directly into that database before invoking the SUT. This approach is most commonly required when we are writing customer tests, but it may also be required for unit tests if the classes we are testing interact directly with the database. We must first determine the pre-conditions of the test and, from that information, identify the data that the test requires for its fixture. We then define a database script that inserts the corresponding records directly into the database bypassing the SUT logic. We use this Database Population Script whenever we want to set up the test fixture—a decision that depends on which test fixture strategy we have chosen. (See Chapter 6, Test Automation Strategy, for more on that topic.)

当决定使用数据库填充脚本时,每当我们修改 SUT 数据存储的结构或其中数据的语义时,我们都需要维护数据库填充脚本及其作为输入的文件。此要求可能会增加测试的维护成本。

When deciding to use a Database Population Script, we will need to maintain both the Database Population Script and the files it takes as input whenever we modify either the structure of the SUT's data stores or the semantics of the data in them. This requirement can increase the maintenance cost of the tests.

变体:数据加载器

数据加载器是一种将数据加载到 SUT 数据存储中的特殊程序。它与数据库填充脚本的不同之处在于,数据加载器是用编程语言而不是数据库语言编写的。这给了我们更多的灵活性,即使系统状态存储在关系数据库以外的其他地方,我们也可以使用数据加载器。

A Data Loader is a special program that loads data into the SUT's data store. It differs from a Database Population Script in that the Data Loader is written in a programming language rather than a database language. This gives us a bit more flexibility and allows us to use the Data Loader even when the system state is stored somewhere other than a relational database.

如果数据存储在 SUT 外部,例如在关系数据库中,则数据加载器可以是写入该数据存储的“另一个应用程序”。它将以与 SUT 大致相同的方式使用数据库,但会从文件而不是 SUT 通常获取输入的地方(例如其他“上游”程序)获取输入。当我们使用对象关系映射 (ORM) 工具从 SUT 访问数据库时,构建数据加载器的一种简单方法是使用数据加载器中的相同域对象和映射。我们只需在内存中创建所需的对象并提交 ORM 的工作单元以将它们保存到数据库中。

If the data store is external to the SUT, such as in a relational database, the Data Loader can be "just another application" that writes to that data store. It would use the database in much the same way as the SUT but would get its inputs from a file rather than from wherever the SUT normally gets its inputs (e.g., other "upstream" programs). When we are using an object relational mapping (ORM) tool to access the database from our SUT, a simple way to build the Data Loader is to use the same domain objects and mappings in our Data Loader. We just create the desired objects in memory and commit the ORM's unit of work to save them into the database.

如果 SUT 将数据存储在内部数据结构中(例如,在内存中),则数据加载器可能需要是 SUT 本身提供的接口。以下特征将其与 SUT 提供的正常功能区分开来:

If the SUT stores data in internal data structures (e.g., in memory), the Data Loader may need to be an interface provided by the SUT itself. The following characteristics differentiate it from the normal functionality provided by the SUT:

  • 它仅供测试使用。
  • It is used only by the tests.
  • 它从文件读取数据,而不是从 SUT 通常获取数据的地方读取数据。
  • It reads the data from a file rather than wherever the SUT normally gets the data.
  • 它绕过了 SUT 通常完成的许多“编辑检查”(输入验证)。
  • It bypasses a lot of the "edit checks" (input validation) normally done by the SUT.

输入文件可以是包含逗号或制表符分隔文本的简单平面文件,也可以使用 XML 进行构造。DbUnit 是 JUnit 的扩展,它实现了用于装置设置的数据加载器

The input files may be simple flat files containing comma- or tab-delimited text, or they could be structured using XML. DbUnit is an extension of JUnit that implements Data Loader for fixture setup.

变体:数据库提取脚本

当 SUT 将其状态存储在运行时访问的数据库中时,我们可以利用此结构进行后门验证。我们只需使用数据库脚本从测试数据库中提取数据,并通过将其与先前准备好的“提取”文件进行比较或确保特定查询返回正确数量的记录来验证它是否包含正确的数据。

When the SUT stores its state in a database that it accesses as it runs, we can take advantage of this structure to do Back Door Verification. We simply use a database script to extract data from the test database and verify that it contains the right data either by comparing it to previously prepared "extract" files or by ensuring that specific queries return the right number of records.

变体:数据检索器

数据检索器类似于数据加载器,它在执行后门验证时从 SUT 检索状态。就像一只值得信赖的狗一样,它“获取”数据,以便我们可以将其与测试中的预期结果进行比较。DbUnit 是 JUnit 的扩展,它实现了数据检索器以支持结果验证。

A Data Retriever is the analog of a Data Loader that retrieves the state from the SUT when doing Back Door Verification. Like a trusty dog, it "fetches" the data so that we can compare it with our expected results within our tests. DbUnit is an extension of JUnit that implements Data Retriever to support result verification.

变体:测试替身作为后门

到目前为止,这里描述的所有实现技术都涉及与 SUT 的 DOC 交互,以设置或拆除夹具,或者验证预期结果。最常见的后门操作形式可能涉及用测试替身替换DOC 。一种方法是使用已预先加载了一些数据的伪对象(第 551页),就好像 SUT 已经与它交互一样;这种策略使我们能够避免使用 SUT 来设置 SUT 的状态。另一种选择是使用某种可配置测试替身(第 558页),比如模拟对象(第 544页) 或测试桩(第 529页)。无论哪种方式,我们都可以通过使测试替身的状态在测试方法(第 348页)中可见来完全避免模糊测试

So far, all of the implementation techniques described here have involved interacting with a DOC of the SUT to set up or tear down the fixture or to verify the expected outcome. Probably the most common form of Back Door Manipulation involves replacing the DOC with a Test Double. One option is to use a Fake Object (page 551) that we have preloaded with some data as though the SUT had already been interacting with it; this strategy allows us to avoid using the SUT to set up the SUT's state. The other option is to use some kind of Configurable Test Double (page 558), such as a Mock Object (page 544) or a Test Stub (page 529). Either way, we can completely avoid Obscure Tests by making the state of the Test Double visible within the Test Method (page 348).

当我们想要对SUT 对一个或多个 DOC 的调用执行行为验证(第468页)时,我们可以使用跨层测试,用测试间谍第 538页)或模拟对象替换 DOC。当我们想要验证 SUT 在从 DOC 接收间接输入时(或在某些特定的外部状态下)是否以特定方式运行,我们可以用测试桩替换 DOC 。

When we want to perform Behavior Verification (page 468) of the calls made by the SUT to one or more DOCs, we can use a layer-crossing test that replaces the DOC with a Test Spy (page 538) or a Mock Object. When we want to verify that the SUT behaves a specific way when it receives indirect inputs from a DOC (or when in some specific external state), we can replace the DOC with a Test Stub.

激励人心的例子

Motivating Example

以下往返测试通过仅通过前门与 SUT 交互来验证移除航班的基本功能。但它不会验证 SUT 的间接输出 - 即每次移除航班时,SUT 都需要调用记录器进行记录,以及发出请求的日期/时间和请求者的用户 ID。在许多系统中,这将是“跨层行为”的一个例子:记录器是通用基础设施层的一部分,而 SUT 是特定于应用程序的行为。

The following round-trip test verifies the basic functionality of removing a flight by interacting with the SUT only via the front door. But it does not verify the indirect outputs of the SUT—namely, that the SUT is expected to call a logger to log each time a flight is removed along with the day/time when the request was made and the user ID of the requester. In many systems, this would be an example of "layer-crossing behavior": The logger is part of a generic infrastructure layer, while the SUT is an application-specific behavior.

public void testRemoveFlight() throws Exception {

    // 设置

    FlightDto expectedFlightDto = createARegisteredFlight();

    FlightManagementFacade Facade = new FlightManagementFacadeImpl();

    // 练习

    Facade.removeFlight(expectedFlightDto.getFlightNumber());

    // 验证

    assertFalse("flight 在被移除后不应存在",

                      Facade.flightExists( expectedFlightDto.

                                                               getFlightNumber()));

}

public  void  testRemoveFlight()  throws  Exception  {

    //  setup

    FlightDto  expectedFlightDto  =  createARegisteredFlight();

    FlightManagementFacade  facade  =  new  FlightManagementFacadeImpl();

    //  exercise

    facade.removeFlight(expectedFlightDto.getFlightNumber());

    //  verify

    assertFalse("flight  should  not  exist  after  being  removed",

                      facade.flightExists(  expectedFlightDto.

                                                               getFlightNumber()));

}

 

重构说明

Refactoring Notes

我们可以通过添加结果验证代码来访问和验证记录器的状态,将此测试转换为使用后门验证。我们可以通过从记录器的数据库中读取该状态来实现这一点,也可以用测试间谍替换记录器,以保存状态以供测试轻松访问。

We can convert this test to use Back Door Verification by adding result verification code to access and verify the logger's state. We can do so either by reading that state from the logger's database or by replacing the logger with a Test Spy that saves the state for easy access by the tests.

示例:使用测试间谍验证后门结果

Example: Back Door Result Verification Using a Test Spy

以下是经过转换的相同测试,使用测试间谍来访问记录器的测试后状态:

Here's the same test converted to use a Test Spy to access the post-test state of the logger:

public void testRemoveFlightLogging_recordingTestStub()

             throws Exception {

      // 固定设置

      FlightDto expectedFlightDto = createAnUnregFlight();

      FlightManagementFacade Facade =

              new FlightManagementFacadeImpl();

      // 测试替身设置

      AuditLogSpy logSpy = new AuditLogSpy();

      Facade.setAuditLog(logSpy);

      // 练习

      Facade.removeFlight(expectedFlightDto.getFlightNumber());

      // 验证

      assertEquals("呼叫次数", 1,

                          logSpy.getNumberOfCalls());

      assertEquals("操作代码",

                          Helper.REMOVE_FLIGHT_ACTION_CODE,

                          logSpy.getActionCode());

      assertEquals("日期", helper.getTodaysDateWithoutTime(),

                          logSpy.getDate());

      assertEquals("用户", Helper.TEST_USER_NAME,

                          logSpy.getUser());

      断言Equals(“详细信息”,

                          expectedFlightDto.getFlightNumber(),

                          logSpy.getDetail());

}

public  void  testRemoveFlightLogging_recordingTestStub()

             throws  Exception  {

      //  fixture  setup

      FlightDto  expectedFlightDto  =  createAnUnregFlight();

      FlightManagementFacade  facade  =

              new  FlightManagementFacadeImpl();

      //      Test  Double  setup

      AuditLogSpy  logSpy  =  new  AuditLogSpy();

      facade.setAuditLog(logSpy);

      //  exercise

      facade.removeFlight(expectedFlightDto.getFlightNumber());

      //  verify

      assertEquals("number  of  calls",  1,

                          logSpy.getNumberOfCalls());

      assertEquals("action  code",

                          Helper.REMOVE_FLIGHT_ACTION_CODE,

                          logSpy.getActionCode());

      assertEquals("date",  helper.getTodaysDateWithoutTime(),

                          logSpy.getDate());

      assertEquals("user",  Helper.TEST_USER_NAME,

                          logSpy.getUser());

      assertEquals("detail",

                          expectedFlightDto.getFlightNumber(),

                          logSpy.getDetail());

}

 

如果记录器的数据库包含太多条目,以至于使用Delta Assertions第 485页)来验证新条目不切实际,那么这种方法将是验证日志记录的更好方法。

This approach would be the better way to verify the logging if the logger's database contained so many entries that it wasn't practical to verify the new entries using Delta Assertions (page 485).

示例:后门装置设置

Example: Back Door Fixture Setup

下一个示例展示了如何使用数据库作为 SUT 的后门来设置装置。测试将一条记录插入表中EmailSubscription,然后要求 SUT 找到它。然后,它对 SUT 返回的对象的各个字段做出断言,以验证记录是否被正确读取。

The next example shows how we can set up a fixture using the database as a back door to the SUT. The test inserts a record into the EmailSubscription table and then asks the SUT to find it. It then makes assertions on various fields of the object returned by the SUT to verify that the record was read correctly.

静态最终字符串 TABLE_NAME = “EmailSubscription”;

静态最终BigDecimal RECORD_ID = new BigDecimal(“111”);



静态最终字符串 LOGIN_ID = “Bob”;

静态最终字符串 EMAIL_ID = “bob@foo.com”;



公共 void setUp() 抛出异常 {

   String xmlString =

             "<?xml version='1.0' encoding='UTF-8'?>" +

             "<dataset>" +

             " <" + TABLE_NAME +

             " EmailSubscriptionId='" + RECORD_ID + "'" +

             " UserLoginId='" + LOGIN_ID + "'" +

             " EmailAddress='" + EMAIL_ID + "'" +

             " RecordVersionNum='62' " +

             " CreateByUserId='MappingTest' " +

             " CreateDateTime='2004-03-01 00:00:00.0' " +

             " LastModByUserId='MappingTest' " +

             " LastModDateTime='2004-03-01 00:00:00.0'/>" +

             "</dataset>";

    insertRowsIntoDatabase(xmlString);

}



public void testRead_Login() throws Exception {

    // 练习

    EmailSubscription subs =

            EmailSubscription.findInstanceWithId(RECORD_ID);

    // 验证

    assertNotNull("电子邮件订阅", subs);

    assertEquals("用户名", LOGIN_ID, subs.getUserName());

}



public void testRead_Email() throws Exception {

    // 练习

    EmailSubscription subs =

                EmailSubscription.findInstanceWithId(RECORD_ID);

    // 验证

    assertNotNull("电子邮件订阅", subs);

    assertEquals("电子邮件地址",

                              EMAIL_ID,

                              subs.getEmailAddress());

}

static  final  String          TABLE_NAME  =  "EmailSubscription";

static  final  BigDecimal  RECORD_ID    =  new  BigDecimal("111");



static  final  String  LOGIN_ID  =  "Bob";

static  final  String  EMAIL_ID  =  "bob@foo.com";



public  void  setUp()  throws  Exception  {

   String  xmlString  =

             "<?xml  version='1.0'  encoding='UTF-8'?>"  +

             "<dataset>"  +

             "        <"  +  TABLE_NAME  +

             "                EmailSubscriptionId='"  +  RECORD_ID  +  "'"  +

             "                UserLoginId='"  +  LOGIN_ID  +  "'"  +

             "                EmailAddress='"  +  EMAIL_ID  +  "'"  +

             "                RecordVersionNum='62'  "  +

             "                CreateByUserId='MappingTest'  "  +

             "                CreateDateTime='2004-03-01  00:00:00.0'    "  +

             "                LastModByUserId='MappingTest'  "  +

             "                LastModDateTime='2004-03-01  00:00:00.0'/>"  +

             "</dataset>";

    insertRowsIntoDatabase(xmlString);

}



public  void  testRead_Login()  throws  Exception  {

    //  exercise

    EmailSubscription  subs  =

            EmailSubscription.findInstanceWithId(RECORD_ID);

    //  verify

    assertNotNull("Email  Subscription",  subs);

    assertEquals("User  Name",  LOGIN_ID,  subs.getUserName());

}



public  void  testRead_Email()  throws  Exception  {

    //  exercise

    EmailSubscription  subs  =

                EmailSubscription.findInstanceWithId(RECORD_ID);

    //  verify

    assertNotNull("Email  Subscription",  subs);

    assertEquals("Email  Address",

                              EMAIL_ID,

                              subs.getEmailAddress());

}

 

用于填充数据库的 XML 文档是在Testcase 类中构建的,这样可以避免使用外部文件加载数据库时产生的神秘客户(参见模糊测试)[内联资源(第736页)重构的讨论解释了这种方法]。为了使测试更清晰,我们调用意图揭示方法来隐藏如何使用 DbUnit 加载数据库的细节,并在测试结束时使用Table Truncation Teardown将其清理掉。以下是本示例中使用的测试实用程序方法的主体:

The XML document used to populate the database is built within the Testcase Class so as to avoid the Mystery Guest (see Obscure Test) that would have been created if we had used an external file for loading the database [the discussion of the In-line Resource (page 736) refactoring explains this approach]. To make the test clearer, we call intent-revealing methods that hide the details of how we use DbUnit to load the database and clean it out at the end of the test using Table Truncation Teardown. Here are the bodies of the Test Utility Methods used in this example:

私有 void insertRowsIntoDatabase(String xmlString)

              抛出异常 {

      IDataSet dataSet = new FlatXmlDataSet(new StringReader(xmlString));

      DatabaseOperation.CLEAN_INSERT.execute

              (getDbConnection(), dataSet);

}



公共 void teaDown() 抛出异常{

      emptyTable(TABLE_NAME);

}



公共 void emptyTable(String tableName) 抛出异常 {

      IDataSet dataSet = new DefaultDataSet(new DefaultTable(tableName));

      DatabaseOperation.DELETE_ALL.execute

                  (getDbConnection(), dataSet);

}

private  void  insertRowsIntoDatabase(String  xmlString)

              throws  Exception  {

      IDataSet  dataSet  =  new  FlatXmlDataSet(new  StringReader(xmlString));

      DatabaseOperation.CLEAN_INSERT.

              execute(  getDbConnection(),  dataSet);

}



public  void  tearDown()  throws  Exception{

      emptyTable(TABLE_NAME);

}



public  void  emptyTable(String  tableName)  throws  Exception  {

      IDataSet  dataSet  =  new  DefaultDataSet(new  DefaultTable(tableName));

      DatabaseOperation.DELETE_ALL.

                  execute(getDbConnection(),  dataSet);

}

 

当然,这些方法的实现特定于 DbUnit;如果我们使用 xUnit 系列的其他成员,我们必须改变它们。

Of course, the implementations of these methods are specific to DbUnit; we must change them if we use some other member of the xUnit family.

对这些测试的其他一些观察:为了避免急切测试(请参阅第 224页的断言轮盘),每个字段的断言都出现在单独的测试中。这种结构可能会导致慢速测试第 253页),因为这些测试与数据库交互。我们可以使用延迟设置(第435页)或套件夹具设置第 441页)来避免多次设置夹具,只要生成的共享夹具(第317页)未被任何测试修改即可。(我选择不通过采用这种方法使这个例子更加复杂。)

Some other observations on these tests: To avoid an Eager Test (see Assertion Roulette on page 224), the assertion on each field appears in a separate test. This structure could result in Slow Tests (page 253) because these tests interact with a database. We could use Lazy Setup (page 435) or Suite Fixture Setup (page 441) to avoid setting up the fixture more than once as long as the resulting Shared Fixture (page 317) was not modified by any of the tests. (I chose not to further complicate this example by taking this tack.)

进一步阅读

请参阅第 336页的侧栏“数据库作为 SUT API? ”来了解后门实际上是前门的示例。

See the sidebar "Database as SUT API?" on page 336 for an example of when the back door is really a front door.


数据库作为 SUT API?

设置测试装置的常用技术是后门设置(请参阅第 327页的后门操作);对于验证测试结果,后门验证(请参阅后门操作)是一种常用选项。但是,什么时候直接与 SUT 后面的数据库交互的测试不被视为通过后门?

在最近的一个项目中,一些朋友正在努力解决这个问题,尽管他们一开始并没有意识到这一点。他们的一位分析师(也是高级用户)似乎过于关注数据库模式。起初,他们将这种狭隘的关注点归咎于分析师的 Powerbuilder 背景,并试图让他改掉这个习惯。但这没有用。分析师只是坚持己见。开发人员试图解释说,在敏捷项目中,重要的是不要在项目开始时尝试定义整个数据模式;相反,模式会随着需求的实现而发展。

当然,每次修改数据库架构时,分析师都会抱怨,因为这些更改会破坏他的所有查询。随着项目的展开,其他团队成员慢慢开始明白,分析师确实需要一个稳定的数据库来运行查询。这是他验证系统生成的数据正确性的方式。

一旦他们认识到这一要求,开发人员就能够将查询模式视为系统提供的正式接口。客户测试是针对此接口编写的,开发人员必须确保无论何时更改数据库,这些测试仍然能够通过。为了最大限度地减少数据库重构的影响,他们定义了一组实现此接口的查询视图。这种方法使他们能够根据需要重构数据库。

您什么时候会遇到这种情况?每当您的客户将报告工具(如 Crystal Reports)应用于您的数据库时,就会有人争论是否需要稳定的报告界面。同样,如果客户使用脚本(如 DTS 或 SQL)将数据加载到数据库中,则可能需要稳定的数据加载界面。



Database as SUT API?

A common technique for setting up test fixtures is Back Door Setup (see Back Door Manipulation on page 327); for verifying test outcomes, Back Door Verification (see Back Door Manipulation) is a popular option. But when is a test that interacts directly with the database behind a SUT not considered to be going through the back door?

On a recent project, some friends were struggling with this very question, though at first they didn't realize it. One of their analysts (who was also a power user) seemed overly focused on the database schema. At first, they put this narrow focus down to the analyst's Powerbuilder background and tried to break him of the habit. That didn't work. The analyst just dug in his heels. The developers tried explaining that on agile projects it was important not to try to define the whole data schema at the beginning of the project; instead, the schema evolved as the requirements were implemented.

Of course, the analyst complained every time they modified the database schema because the changes broke all his queries. As the project unfolded, the other team members slowly started to understand that the analyst really did need a stable database against which to run queries. It was his way to verify the correctness of the data generated by the system.

Once they recognized this requirement, the developers were able to treat the query schema as a formal interface provided by the system. Customer tests were written against this interface and developers had to ensure that those tests still passed whenever they changed the database. To minimize the impact of database refactorings, they defined a set of query views that implemented this interface. This approach allowed them to refactor the database as needed.

When might you find yourself in this situation? Any time your customer applies reporting tools (such as Crystal Reports) to your database, an argument can be made as to whether part of the requirements is a stable reporting interface. Similarly, if the customer uses scripts (such as DTS or SQL) to load data into the database, there may be a requirement for a stable data loading interface.


 

层测试

Layer Test

也称为

Also known as

单层测试、分层测试、分层测试

Single Layer Test, Testing by Layers, Layered Test

当逻辑是分层架构的一部分时,我们如何独立验证逻辑?

How can we verify logic independently when it is part of a layered architecture?

我们为分层架构的每一层编写单独的测试。

We write separate tests for each layer of the layered architecture.

图像

当以自上而下的方式测试整个应用程序时,很难获得良好的测试覆盖率;我们最终必然会对应用程序的某些部分进行间接测试(请参阅第186页的模糊测试)。许多应用程序使用分层架构 [DDD、PEAA、WWW] 来分离主要的技术问题。大多数应用程序都有某种表示(用户界面)层、业务逻辑层或域层以及持久层。一些分层架构甚至有更多层。

It is difficult to obtain good test coverage when testing an entire application in a top-to-bottom fashion; we are bound to end up doing Indirect Testing (see Obscure Test on page 186) on some parts of the application. Many applications use a Layered Architecture [DDD, PEAA, WWW] to separate the major technical concerns. Most applications have some kind of presentation (user interface) layer, a business logic layer or domain layer, and a persistence layer. Some layered architectures have even more layers.

通过单独测试每一层,可以更有效地测试具有分层架构的应用程序。

An application with a layered architecture can be tested more effectively by testing each layer in isolation.

工作原理

How It Works

我们使用分层架构设计 SUT,将表示逻辑与业务逻辑以及任何持久性机制或与其他系统的接口分开。11我们将所有业务逻辑放入服务层[PEAA],该服务层将应用程序功能作为API暴露给表示层。我们将架构的每一层视为一个单独的 SUT。我们为架构中的每一层编写独立于其他层的组件测试。也就是说,对于n架构的层,测试将取代层;我们可以选择用测试替身n+1替换层(第 522页)。n-1

We design the SUT using a layered architecture that separates the presentation logic from the business logic and from any persistence mechanism or interfaces to other systems.11 We put all business logic into a Service Layer [PEAA] that exposes the application functionality to the presentation layer as an API. We treat each layer of the architecture as a separate SUT. We write component tests for each layer independent of the other layers of the architecture. That is, for layer n of the architecture, the tests will take the place of layer n+1; we may optionally replace layer n-1 with a Test Double (page 522).

何时使用它

When to Use It

每当我们拥有分层架构并且想要为每一层的逻辑提供良好的测试覆盖率时,我们就可以使用层测试。独立测试每一层比同时测试所有层要简单得多。当我们想要对跨层边界调用的返回值进行防御性编码时尤其如此。在正常运行的软件中,这些错误“不应该发生”;但在现实生活中,它们确实会发生。为了确保我们的代码处理这些错误,我们可以将这些“永远不会发生”的场景作为间接输入注入到我们的层中。

We can use a Layer Test whenever we have a layered architecture and we want to provide good test coverage of the logic in each layer. It can be much simpler to test each layer independently than it is to test all the layers at once. This is especially true when we want to do defensive coding for return values of calls across the layer boundary. In software that is working correctly, these errors "should never happen"; in real life, they do. To make sure our code handles these errors, we can inject these "never happen" scenarios as indirect inputs to our layer.

当我们想根据团队成员擅长的技术将项目团队划分为子团队时,层测试非常有用。架构的每一层往往需要不同的知识,并且经常使用不同的技术;因此,层边界充当了自然的团队边界。层测试可以成为确定和记录层接口语义的好方法。

Layer Tests are very useful when we want to divide up the project team into subteams based on the technology in which the team members specialize. Each layer of an architecture tends to require different knowledge and often uses different technologies; therefore, the layer boundaries serve as natural team boundaries. Layer Tests can be a good way to nail down and document the semantics of the layer interfaces.

即使我们选择使用层测试策略,最好也包含一些“自上而下”的测试,以验证各个层是否正确集成。这些测试只需涵盖一两个基本场景;我们不需要测试每个业务测试条件,因为所有这些条件都已在至少一个层的层测试中进行了测试。

Even when we choose to use a Layer Test strategy, it is a good idea to include a few "top-to-bottom" tests just to verify that the various layers are integrated correctly. These tests need to cover only one or two basic scenarios; we don't need to test every business test condition because all of them have already been tested in the Layer Tests for at least one of the layers.

该模式的大多数变化反映了哪一层是独立于其他层进行测试的。

Most of the variations on this pattern reflect which layer is being tested independently of the other layers.

变体:表示层测试

仅介绍表示层测试模式就可以写出一整本书。具体模式取决于表示层技术的性质(例如,图形用户界面、传统 Web 界面、“智能”Web 界面、Web 服务)。无论采用哪种技术,关键是要将表示逻辑与业务逻辑分开测试,这样我们就不必担心底层逻辑的变化会影响我们的表示层测试。(它们本身就很难实现自动化!)

One could write a whole book just on patterns of presentation layer testing. The specific patterns depend on the nature of the presentation layer technology (e.g., graphical user interface, traditional Web interface, "smart" Web interface, Web services). Regardless of the technology, the key is to test the presentation logic separately from the business logic so that we don't have to worry about changes in the underlying logic affecting our presentation layer tests. (They are hard enough to automate well as it is!)

另一个考虑因素是设计表示层,以便可以独立于表示框架测试其逻辑。Humble Dialog(请参见第695页的Humble Object)是此处应用的关键可测试性设计模式。实际上,我们在表示层中定义子层;包含Humble Dialogs 的层是“表示图形层”,而我们使其可测试的层是“表示行为层”。这种层分离使我们能够验证按钮是否已激活、菜单项是否变灰等,而无需实例化任何实际图形对象。

Another consideration is to design the presentation layer so that its logic can be tested independently of the presentation framework. Humble Dialog (see Humble Object on page 695) is the key design-for-testability pattern to apply here. In effect, we are defining sublayers within the presentation layer; the layer containing the Humble Dialogs is the "presentation graphic layer" and the layer we have made testable is the "presentation behavior layer." This separation of layers allows us to verify that buttons are activated, menu items are grayed out, and so on, without instantiating any of the real graphical objects.

变体:服务层测试

服务层是我们大多数单元测试和组件测试传统上集中的地方。使用客户测试来测试业务逻辑更具挑战性,因为通过表示层测试服务层通常涉及间接测试敏感平等(请参阅第239页的脆弱测试这两者都可能导致脆弱测试高测试维护成本(第265页)。直接测试服务层有助于避免这些问题。

The Service Layer is where most of our unit tests and component tests are traditionally concentrated. Testing the business logic using customer tests is a bit more challenging because testing the Service Layer via the presentation layer often involves Indirect Testing and Sensitive Equality (see Fragile Test on page 239), either of which can lead to Fragile Tests and High Test Maintenance Cost (page 265). Testing the Service Layer directly helps avoid these problems.

为了避免测试速度慢第 253页),我们通常用伪数据库(参见第 551页的伪对象)替换持久层,然后运行测试。事实上,分层架构背后的大部分推动力是将此代码与其他难以测试的层隔离开来。Alistair Cockburn 在http://alistair.cockburn.us [WWW]上对六边形架构的描述中对这个想法进行了有趣的阐述。

To avoid Slow Tests (page 253), we usually replace the persistence layer with a Fake Database (see Fake Object on page 551) and then run the tests. In fact, most of the impetus behind a layered architecture is to isolate this code from the other, harder-to-test layers. Alistair Cockburn puts an interesting spin on this idea in his description of a Hexagonal Architecture at http://alistair.cockburn.us [WWW].

服务层可能在其他用途​​上有用。它可用于以“无头”模式(不附加表示层)运行应用程序,例如使用宏来自动执行 Microsoft Excel 中经常执行的任务。

The Service Layer may come in handy for other uses. It can be used to run the application in "headless" mode (without a presentation layer attached), such as when using macros to automate frequently done tasks in Microsoft Excel.

变体:持久层测试

持久层也需要测试。如果应用程序是唯一使用数据存储的应用程序,往返测试通常就足够了。但这些测试无法捕获一种编程错误:当我们不小心将信息放入错误的列中时。只要交换列的数据类型兼容并且我们在读取数据时犯了相同的错误,我们的往返测试就会通过!这种错误不会影响我们应用程序的运行,但它可能会使支持更加困难,并会导致与其他应用程序交互时出现问题。

The persistence layer also needs to be tested. Round-trip tests will often suffice if the application is the only one that uses the data store. But these tests won't catch one kind of programming error: when we accidentally put information into the wrong columns. As long as the data type of the interchanged columns is compatible and we make the same error when reading the data, our round-trip tests will pass! This kind of bug won't affect the operation of our application but it might make support more difficult and it will cause problems in interactions with other applications.

当其他应用程序也使用数据存储时,强烈建议至少实施几个跨层测试,以验证信息是否放入了表的正确列中。我们可以使用后门操作第 327页)来设置数据库内容或验证测试后的数据库内容。

When other applications also use the data store, it is highly advisable to implement at least a few layer-crossing tests that verify information is put into the correct columns of tables. We can use Back Door Manipulation (page 327) to either set up the database contents or to verify the post-test database contents.

变化:皮下试验

皮下测试层测试的退化形式,它绕过系统的表示层直接与服务层交互。在大多数情况下,服务层并不与下面的层隔离;因此,我们测试除表示层之外的所有内容。使用皮下测试不需要像服务层测试那样严格地分离关注点,这使得当我们将测试改装到未针对可测试性设计的应用程序上时,皮下测试更容易使用。每当我们为应用程序编写客户测试并且希望确保我们的测试是稳健的时,我们都应该使用皮下测试。皮下测试不太可能因应用程序12的更改而受到破坏,因为它不通过表示层与应用程序交互;因此,整个类别的更改不会影响它。

A Subcutaneous Test is a degenerate form of Layer Test that bypasses the presentation layer of the system to interact directly with the Service Layer. In most cases, the Service Layer is not isolated from the layer(s) below; therefore, we test everything except the presentation. Use of a Subcutaneous Test does not require as strict a separation of concerns as does a Service Layer Test, which makes Subcutaneous Test easier to use when we are retrofitting tests onto an application that wasn't designed for testability. We should use a Subcutaneous Test whenever we are writing customer tests for an application and we want to ensure our tests are robust. A Subcutaneous Test is much less likely to be broken by changes to the application12 because it does not interact with the application via the presentation layer; as a consequence, a whole category of changes won't affect it.

变体:组件测试

组件测试是层测试的最常见形式,我们可以将层视为由充当“微层”的单个组件组成。当我们进行基于组件的开发并且某些组件必须修改或从头开始构建时,组件测试是指定或记录单个组件行为的好方法。

A Component Test is the most general form of Layer Test, in that we can think of the layers being made up of individual components that act as "micro-layers." Component Tests are a good way to specify or document the behavior of individual components when we are doing component-based development and some of the components must be modified or built from scratch.

实施说明

Implementation Notes

我们可以将层测试编写为往返测试或跨层测试。每种方法都有优点。在实践中,我们通常混合使用这两种测试风格。往返测试更容易编写(假设我们已经有一个合适的Fake Object可用于层n-1)。然而,当我们验证层中的错误处理逻辑时,我们需要使用跨层测试n

We can write our Layer Tests as either round-trip tests or layer-crossing tests. Each has advantages. In practice, we typically mix both styles of tests. The round-trip tests are easier to write (assuming we already have a suitable Fake Object available to use for layer n-1). We need to use layer-crossing tests, however, when we are verifying the error-handling logic in layer n.

往返测试

层测试的一个良好起点是往返测试,因为它应该足以满足大多数简单成功测试的要求(请参阅第348页的测试方法)。这些测试可以这样编写,即它们不关心我们是否已将感兴趣的层与下面的层完全隔离。我们可以保留真实组件,以便间接地执行它们,也可以用假对象替换它们。当数据库或下面层的异步机制导致测试速度变慢时,后一种选择特别有用。

A good starting point for Layer Tests is the round-trip test, as it should be sufficient for most Simple Success Tests (see Test Method on page 348). These tests can be written such that they do not care whether we have fully isolated the layer of interest from the layers below. We can either leave the real components in place so that they are exercised indirectly, or we can replace them with Fake Objects. The latter option is particularly useful when by a database or asynchronous mechanisms in the layer below lead to Slow Tests.

控制间接投入

我们可以用测试桩第 529页)替换系统的较低层,该桩根据客户端层在请求中传递的内容返回“预设”结果(例如,客户 0001 是有效客户,0002 是休眠客户,0003 有三个帐户)。这种技术使我们能够使用来自下层的易于理解的间接输入来测试客户端逻辑。当我们自动化预期异常测试(请参阅测试方法)或执行依赖于来自上游系统的数据的行为时,它特别有用。13另一种方法是使用后门操作来设置间接输入。

We can replace a lower layer of the system with a Test Stub (page 529) that returns "canned" results based on what the client layer passes in a request (e.g., Customer 0001 is a valid customer, 0002 is a dormant customer, 0003 has three accounts). This technique allows us to test the client logic with well-understood indirect inputs from the layer below. It is particularly useful when we are automating Expected Exception Tests (see Test Method) or when we are exercising behavior that depends on data that arrives from an upstream system.13 The alternative is to use Back Door Manipulation to set up the indirect inputs.

验证间接输出

当我们想要验证相关层的间接输出时,我们可以使用Mock Object(第544页)或Test Spy(第538页)来替换 SUT 下层中的组件。然后,我们可以将对 DOC 的实际调用与预期调用进行比较。另一种方法是使用后门操作来验证 SUT 的间接输出(在它们发生之后)。

When we want to verify the indirect outputs of the layer of interest, we can use a Mock Object (page 544) or Test Spy (page 538) to replace the components in the layer below the SUT. We can then compare the actual calls made to the DOC with the expected calls. The alternative is to use Back Door Manipulation to verify the indirect outputs of the SUT after they have occurred.

激励人心的例子

Motivating Example

当尝试同时测试应用程序的所有层时,我们必须通过表示层来验证业务逻辑的正确性。以下测试是一个非常简单的示例,通过简单的用户界面测试一些简单的业务逻辑:

When trying to test all layers of the application at the same time, we must verify the correctness of the business logic through the presentation layer. The following test is a very simple example of testing some trivial business logic through a trivial user interface:

private final int LEGAL_CONN_MINS_SAME = 30;

public void testAnalyze_sameAirline_LessThanConnectionLimit()

throws Exception {

    // 设置

    FlightConnection illegalConn =

            createSameAirlineConn( LEGAL_CONN_MINS_SAME - 1);

    // 练习

    FlightConnectionAnalyzerImpl sut =

             new FlightConnectionAnalyzerImpl();

    String actualHtml =

             sut.getFlightConnectionAsHtmlFragment(

                           illegalConn.getInboundFlightNumber(),

                           illegalConn.getOutboundFlightNumber());

    // 验证

    StringBuffer expected = new StringBuffer();

    expected.append("<span class="boldRedText">");

    expected.append("航班之间的连接时间 ");

    expected.append(illegalConn.getInboundFlightNumber());

    expected.append(" 和航班 ");

    预期.append(illegalConn.getOutboundFlightNumber());

    预期.append(" 是 ");

    预期.append(illegalConn.getActualConnectionTime());

    预期.append(" 分钟。</span>");

    assertEquals("html", expected.toString(), actualHtml);

}

private  final  int  LEGAL_CONN_MINS_SAME  =  30;

public  void  testAnalyze_sameAirline_LessThanConnectionLimit()

throws  Exception  {

    //  setup

    FlightConnection  illegalConn  =

            createSameAirlineConn(  LEGAL_CONN_MINS_SAME  -  1);

    //  exercise

    FlightConnectionAnalyzerImpl  sut  =

             new  FlightConnectionAnalyzerImpl();

    String  actualHtml  =

             sut.getFlightConnectionAsHtmlFragment(

                           illegalConn.getInboundFlightNumber(),

                           illegalConn.getOutboundFlightNumber());

    //  verification

    StringBuffer  expected  =  new  StringBuffer();

    expected.append("<span  class="boldRedText">");

    expected.append("Connection  time  between  flight  ");

    expected.append(illegalConn.getInboundFlightNumber());

    expected.append("  and  flight  ");

    expected.append(illegalConn.getOutboundFlightNumber());

    expected.append("  is  ");

    expected.append(illegalConn.getActualConnectionTime());

    expected.append("  minutes.</span>");

    assertEquals("html",  expected.toString(),  actualHtml);

}

 

此测试包含有关业务层功能(什么导致连接非法)和表示层功能(如何呈现非法连接)的知识。它还依赖于数据库,因为FlightConnections 是从另一个组件检索的。如果这些领域中的任何一个发生变化,也必须重新进行此测试。

This test contains knowledge about the business layer functionality (what makes a connection illegal) and presentation layer functionality (how an illegal connection is presented). It also depends on the database because the FlightConnections are retrieved from another component. If any of these areas change, this test must be revisited as well.

重构说明

Refactoring Notes

我们可以将此测试拆分为两个单独的测试:一个测试业务逻辑(什么构成非法连接?),另一个测试表示层(给定非法连接,应如何向用户显示?)。我们通常会通过复制整个测试用例类(第 373页),从业务层测试方法中剥离表示层逻辑验证,并在表示层测试方法中桩业务层对象来实现这一点。

We can split this test into two separate tests: one to test the business logic (What constitutes an illegal connection?) and one to test the presentation layer (Given an illegal connection, how should it be displayed to the user?). We would typically do so by duplicating the entire Testcase Class (page 373), stripping out the presentation layer logic verification from the business layer Test Methods, and stubbing out the business layer object(s) in the presentation layer Test Methods.

在此过程中,我们可能会发现,我们可以减少至少一个测试用例类中的测试数量,因为该层存在的测试条件很少。在这个例子中,我们开始进行四个测试(相同/不同航空公司和时间段的组合),每个测试都测试业务层和表示层;我们最终在业务层进行了四个测试(原始组合但直接测试)并在表示层进行了两个测试(合法和非法连接的格式)。14因此,只有后两个测试需要关注字符串格式的细节,当测试失败时,我们就知道哪个层存在错误。

Along the way, we will probably find that we can reduce the number of tests in at least one of the Testcase Classes because few test conditions exist for that layer. In this example, we started out with four tests (the combinations of same/different airlines and time periods), each of which tested both the business and presentation layers; we ended up with four tests in the business layer (the original combinations but tested directly) and two tests in the presentation layer (formatting of legal and illegal connections).14 Therefore, only the latter two tests need to be concerned with the details of the string formatting and, when a test fails, we know which layer holds the bug.

我们可以进一步进行重构,通过使用“用测试替身替换依赖项”第 739页)重构,将此皮下测试转变为真正的服务层测试

We can take our refactoring even further by using a Replace Dependency with Test Double (page 739) refactoring to turn this Subcutaneous Test into a true Service Layer Test.

示例:表示层测试

Example: Presentation Layer Test

以下示例展示了重构后的早期测试,用于验证请求非法连接时表示层的行为。它删除了 ,FlightConnAnalyzer并使用非法连接对其进行配置,以便在调用时返回HtmlFacade。这种技术使我们能够完全控制 SUT 的间接输入。

The following example shows the earlier test refactored to verify the behavior of the presentation layer when an illegal connection is requested. It stubs out the FlightConnAnalyzer and configures it with the illegal connection to return to the HtmlFacade when it is called. This technique gives us complete control over the indirect input of the SUT.

public void testGetFlightConnAsHtml_illegalConnection()

throws Exception {

    // 设置

    FlightConnection illegalConn = createIllegalConnection();

    Mock analyzerStub = mock(IFlightConnAnalyzer.class);

    analyzerStub.expects(once()).method("analyze")

            .will(returnValue(illegalConn));

    HTMLFacade htmlFacade =

            new HTMLFacade((IFlightConnAnalyzer)analyzerStub.proxy());

    // 练习

    String actualHtmlString =

            htmlFacade.getFlightConnectionAsHtmlFragment(

                                illegalConn.getInboundFlightNumber(),

                                illegalConn.getOutboundFlightNumber());

    // 验证

    StringBuffer expected = new StringBuffer();

    expected.append("<span class="boldRedText">");

    expected.append("航班间连接时间 ");

    预期.append(illegalConn.getInboundFlightNumber());

    预期.append(" 和航班 ");

    预期.append(illegalConn.getOutboundFlightNumber());

    预期.append(" 是 ");

    预期.append(illegalConn.getActualConnectionTime());

    预期.append(" 分钟。</span>");

    assertEquals("返回 HTML",

                       expected.toString(),

                       actualHtmlString);

}

public  void  testGetFlightConnAsHtml_illegalConnection()

throws  Exception  {

    //  setup

    FlightConnection  illegalConn  =  createIllegalConnection();

    Mock  analyzerStub  =  mock(IFlightConnAnalyzer.class);

    analyzerStub.expects(once()).method("analyze")

            .will(returnValue(illegalConn));

    HTMLFacade  htmlFacade  =

            new  HTMLFacade((IFlightConnAnalyzer)analyzerStub.proxy());

    //  exercise

    String  actualHtmlString  =

            htmlFacade.getFlightConnectionAsHtmlFragment(

                                illegalConn.getInboundFlightNumber(),

                                illegalConn.getOutboundFlightNumber());

    //  verify

    StringBuffer  expected  =  new  StringBuffer();

    expected.append("<span  class="boldRedText">");

    expected.append("Connection  time  between  flight  ");

    expected.append(illegalConn.getInboundFlightNumber());

    expected.append("  and  flight  ");

    expected.append(illegalConn.getOutboundFlightNumber());

    expected.append("  is  ");

    expected.append(illegalConn.getActualConnectionTime());

    expected.append("  minutes.</span>");

    assertEquals("returned  HTML",

                       expected.toString(),

                       actualHtmlString);

}

 

我们必须比较 HTML 的字符串表示,以确定代码是否生成了正确的响应。幸运的是,我们只需要两个这样的测试就可以验证此组件的基本行为。

We must compare the string representations of the HTML to determine whether the code has generated the correct response. Fortunately, we need only two such tests to verify the basic behavior of this component.

例如:皮下试验

Example: Subcutaneous Test

这是将原始测试转换为皮下测试,绕过表示层来验证连接信息是否计算正确。请注意,此测试中没有任何字符串操作。

Here's the original test converted into a Subcutaneous Test that bypasses the presentation layer to verify that the connection information is calculated correctly. Note the lack of any string manipulation in this test.

private final int LEGAL_CONN_MINS_SAME = 30;

public void testAnalyze_sameAirline_LessThanConnectionLimit()

throws Exception {

    // 设置

    FlightConnection expectedConnection =

           createSameAirlineConn( LEGAL_CONN_MINS_SAME -1);

    // 练习

    IFlightConnAnalyzer theConnectionAnalyzer =

           new FlightConnAnalyzer();

    FlightConnection actualConnection =

            theConnectionAnalyzer.getConn(

                      expectedConnection.getInboundFlightNumber(),

                      expectedConnection.getOutboundFlightNumber());

    // 验证

    assertNotNull("实际连接", actualConnection);

    assertFalse("IsLegal", actualConnection.isLegal());

}

private  final  int  LEGAL_CONN_MINS_SAME  =  30;

public  void  testAnalyze_sameAirline_LessThanConnectionLimit()

throws  Exception  {

    //  setup

    FlightConnection  expectedConnection  =

           createSameAirlineConn(  LEGAL_CONN_MINS_SAME  -1);

    //  exercise

    IFlightConnAnalyzer  theConnectionAnalyzer  =

           new  FlightConnAnalyzer();

    FlightConnection  actualConnection  =

            theConnectionAnalyzer.getConn(

                      expectedConnection.getInboundFlightNumber(),

                      expectedConnection.getOutboundFlightNumber());

    //  verification

    assertNotNull("actual  connection",  actualConnection);

    assertFalse("IsLegal",  actualConnection.isLegal());

}

 

虽然我们绕过了表示层,但我们并未尝试将服务层与下面的各层隔离。这种疏忽可能会导致测试缓慢不稳定(第228页)。

While we have bypassed the presentation layer, we have not attempted to isolate the Service Layer from the layers below. This omission could result in Slow Tests or Erratic Tests (page 228).

示例:业务层测试

Example: Business Layer Test

下一个示例展示了将同一测试转换为与其下层完全隔离的服务层测试。我们使用 JMock 将这些组件替换为模拟对象,以验证是否正在查找正确的航班,并将构建的相应航班注入 SUT。

The next example shows the same test converted into a Service Layer Test that is fully isolated from the layers below it. We have used JMock to replace these components with Mock Objects that verify the correct flights are being looked up and that inject the corresponding flight constructed into the SUT.

public void testAnalyze_sameAirline_EqualsConnectionLimit()

throws Exception {

    // 设置

    Mock flightMgntStub = mock(FlightManagementFacade.class);

    Flight firstFlight = createFlight();

    Flight secondFlight = createConnectingFlight(

                                      firstFlight, LEGAL_CONN_MINS_SAME);

    flightMgntStub.expects(once()).method("getFlight")

                           .with(eq(firstFlight.getFlightNumber()))

                           .will(returnValue(firstFlight));

    flightMgntStub.expects(once()).method("getFlight")

                           .with(eq(secondFlight.getFlightNumber()))

                           .will(returnValue(secondFlight));

    // 锻炼

    FlightConnAnalyzer theConnectionAnalyzer = new FlightConnAnalyzer();

    theConnectionAnalyzer.facade =

            (FlightManagementFacade)flightMgntStub.proxy();

    FlightConnection actualConnection =

           theConnectionAnalyzer.getConn(

                                         firstFlight.getFlightNumber(),

                                         secondFlight.getFlightNumber());

    // 验证

    assertNotNull("实际连接", actualConnection);

    assertTrue("IsLegal", actualConnection.isLegal());

}

public  void  testAnalyze_sameAirline_EqualsConnectionLimit()

throws  Exception  {

    //  setup

    Mock  flightMgntStub  =  mock(FlightManagementFacade.class);

    Flight  firstFlight  =  createFlight();

    Flight  secondFlight  =  createConnectingFlight(

                                      firstFlight,  LEGAL_CONN_MINS_SAME);

    flightMgntStub.expects(once()).method("getFlight")

                           .with(eq(firstFlight.getFlightNumber()))

                           .will(returnValue(firstFlight));

    flightMgntStub.expects(once()).method("getFlight")

                           .with(eq(secondFlight.getFlightNumber()))

                           .will(returnValue(secondFlight));

    //  exercise

    FlightConnAnalyzer  theConnectionAnalyzer  =  new  FlightConnAnalyzer();

    theConnectionAnalyzer.facade  =

            (FlightManagementFacade)flightMgntStub.proxy();

    FlightConnection  actualConnection  =

           theConnectionAnalyzer.getConn(

                                         firstFlight.getFlightNumber(),

                                         secondFlight.getFlightNumber());

    //  verification

    assertNotNull("actual  connection",  actualConnection);

    assertTrue("IsLegal",  actualConnection.isLegal());

}

 

由于服务层与任何底层完全隔离,因此该测试运行速度非常快。由于测试的代码少得多,因此它也可能更加健壮。

This test runs very quickly because the Service Layer is fully isolated from any underlying layers. It is also likely to be much more robust because it tests much less code.

第十九章

xUnit基础模式

Chapter 19

xUnit Basics Patterns

 

本章中的模式

Patterns in This Chapter

测试定义

Test Definition

      

测试方法 348

      

Test Method 348

            

四相测试 358

            

Four-Phase Test 358

      

断言方法 362

      

Assertion Method 362

            

断言消息 370

            

Assertion Message 370

      

测试用例类 373

      

Testcase Class 373

测试执行

Test Execution

      

测试运行器 377

      

Test Runner 377

      

测试用例对象 382

      

Testcase Object 382

      

测试套件对象 387

      

Test Suite Object 387

      

测试发现 393

      

Test Discovery 393

      

测试枚举 399

      

Test Enumeration 399

      

测试选择 403

      

Test Selection 403

测试方法

Test Method

我们的测试代码应该放在哪里?

Where do we put our test code?

我们将每个测试编码为某个类的单个测试方法。

We encode each test as a single Test Method on some class.

图像

全自动测试(见第 26)由测试逻辑组成。在我们编译和执行之前,该逻辑必须存在某个地方。

Fully Automated Tests (see page 26) consist of test logic. That logic has to live somewhere before we can compile and execute it.

工作原理

How It Works

我们将每个测试定义为实现全自动测试所需的四个阶段(见第358页的四阶段测试)的方法、过程或功能。最值得注意的是,如果测试方法要进行自检测试,则必须包含断言(见第26页)。

We define each test as a method, procedure, or function that implements the four phases (see Four-Phase Test on page 358) necessary to realize a Fully Automated Test. Most notably, the Test Method must include assertions if it is to be a Self-Checking Test (see page 26).

我们按照标准测试方法模板之一组织测试逻辑,以便测试读者轻松识别测试类型。在简单成功测试中,我们有一个纯线性的控制流,从夹具设置到执行 SUT 再到结果验证。在预期异常测试中,基于语言的结构将我们引导到错误处理代码。如果我们到达该代码,我们就通过了测试;如果没有,我们就失败了。在构造函数测试中,我们只需实例化一个对象并对其属性进行断言。

We organize the test logic following one of the standard Test Method templates to make the type of test easily recognizable by test readers. In a Simple Success Test, we have a purely linear flow of control from fixture setup through exercising the SUT to result verification. In an Expected Exception Test, language-based structures direct us to error-handling code. If we reach that code, we pass the test; if we don't, we fail it. In a Constructor Test, we simply instantiate an object and make assertions against its attributes.

我们为什么这样做

Why We Do This

我们必须在某个地方对测试逻辑进行编码。在程序世界中,我们将每个测试编码为位于文件或模块中的测试用例程序。在面向对象的编程语言中,首选方案是将它们编码为合适的测试用例类(373页) 上的方法,然后在运行时使用测试发现(第 393)或测试枚举(第 399 页) 将这些测试方法转换为测试用例对象(第 382页)。

We have to encode the test logic somewhere. In the procedural world, we would encode each test as a test case procedure located in a file or module. In object-oriented programming languages, the preferred option is to encode them as methods on a suitable Testcase Class (page 373) and then to turn these Test Methods into Testcase Objects (page 382) at runtime using either Test Discovery (page 393) or Test Enumeration (page 399).

我们遵循标准测试模板,以使我们的测试方法尽可能简单。这极大地提高了它们作为系统文档的实用性(参见第23页),因为它使查找 SUT 基本行为的描述变得更加容易。如果预期异常测试仅包含错误处理语言结构(例如try/catch),那么识别哪些测试描述了此基本行为就会容易得多。

We follow the standard test templates to keep our Test Methods as simple as possible. This greatly increases their utility as system documentation (see page 23) by making it easier to find the description of the basic behavior of the SUT. It is a lot easier to recognize which tests describe this basic behavior if only Expected Exception Tests contain error-handling language constructs such as try/catch.

实施说明

Implementation Notes

我们仍然需要一种方法来运行Testcase Class上的所有测试方法测试。一种解决方案是在Testcase Class上定义一个静态方法,该方法调用每个测试方法。当然,我们还必须计算测试数量,确定通过的测试数量和失败的测试数量。因为测试套件无论如何都需要此功能,所以一个简单的解决方案是实例化一个测试套件对象(第 387页) 来保存每个测试方法。1如果我们使用测试发现测试枚举为每个测试方法创建Testcase Class的实例,则此方法很容易实现

We still need a way to run all the Test Methods tests on the Testcase Class. One solution is to define a static method on the Testcase Class that calls each of the test methods. Of course, we would also have to deal with counting the tests and determining how many passed and how many failed. Because this functionality is needed for a test suite anyway, a simple solution is to instantiate a Test Suite Object (page 387) to hold each Test Method.1 This approach is easy to implement if we create an instance of the Testcase Class for each Test Method using either Test Discovery or Test Enumeration.

在 Java 和 C# 等静态类型语言中,我们可能必须将一个throws子句作为测试方法声明的一部分,这样编译器就不会抱怨我们没有处理 SUT 声明可能抛出的已检查异常。实际上,我们告诉编译器“测试运行器(第 377页) 将处理这些异常。”

In statically typed languages such as Java and C#, we may have to include a throws clause as part of the Test Method declaration so the compiler won't complain about the fact that we are not handling the checked exceptions that the SUT has declared it may throw. In effect, we tell the compiler that "The Test Runner (page 377) will deal with the exceptions."

当然,不同类型的功能需要不同类型的测试方法。然而,几乎所有测试都可以归结为三种基本类型之一。

Of course, different kinds of functionality need different kinds of Test Methods. Nevertheless, almost all tests can be boiled down to one of three basic types.

变体:简单的成功测试

大多数软件都有明显的成功场景(或“快乐路径”)。简单成功测试以一种简单且易于识别的方式验证成功场景。我们创建 SUT 的实例并调用我们要测试的方法。然后我们断言预期结果已经发生。换句话说,我们遵循四阶段测试的正常步骤。我们不会捕获可能发生的任何异常。相反,我们让测试自动化框架第 298页)捕获并报告它们。否则会导致模糊测试第 186页),并会误导测试读者,使其看起来像是预期的异常。请参阅测试作为文档以了解此方法背后的原理。

Most software has an obvious success scenario (or "happy path"). A Simple Success Test verifies the success scenario in a simple and easily recognized way. We create an instance of the SUT and call the method(s) that we want to test. We then assert that the expected outcome has occurred. In other words, we follow the normal steps of a Four-Phase Test. What we don't do is catch any exceptions that could happen. Instead, we let the Test Automation Framework (page 298) catch and report them. Doing otherwise would result in Obscure Tests (page 186) and would mislead the test reader by making it appear as if exceptions were expected. See Tests as Documentation for the rationale behind this approach.

避免使用 -style 代码的另一个好处try/catch是,当错误确实发生时,更容易追踪到错误,因为测试自动化框架报告的是 SUT 深处实际发生错误的位置,而不是我们在测试中调用断言方法(第 362页)(如fail或 )的位置assertTrue。这些类型的错误比断言失败更容易排除故障。

Another benefit of avoiding try/catch-style code is that when errors do occur, it is a lot easier to track them down because the Test Automation Framework reports the location where the actual error occurred deep in the SUT rather than the place in our test where we called an Assertion Method (page 362) such as fail or assertTrue. These kinds of errors turn out to be much easier to troubleshoot than assertion failures.

变体:预期异常测试

编写通过简单成功测试的软件非常简单。软件中的大多数缺陷都出现在各种替代路径中 — 尤其是与错误场景相关的路径,因为这些场景通常是未经测试的需求(请参阅第268页上的“生产错误”)或未经测试的代码(请参阅“生产错误”)。预期异常测试可帮助我们验证错误场景是否已正确编码。我们设置测试装置并以可能导致错误的每一种方式运行 SUT。我们使用我们可用的任何语言结构来捕获错误,以确保发生了预期的错误。如果引发错误,流程将传递到错误处理块。这种转移可能足以让测试通过,但如果异常或错误的类型或消息内容很重要(例如,何时向用户显示错误消息),我们可以使用相等断言(请参阅“断言方法”)来验证它。如果没有引发错误,我们将调用以报告 SUT 未能按预期引发错误。fail

Writing software that passes the Simple Success Test is pretty straightforward. Most of the defects in software appear in the various alternative paths—especially the ones that relate to error scenarios, because these scenarios are often Untested Requirements (see Production Bugs on page 268) or Untested Code (see Production Bugs). An Expected Exception Test helps us verify that the error scenarios have been coded correctly. We set up the test fixture and exercise the SUT in each way that should result in an error. We ensure that the expected error has occurred by using whatever language construct we have available to catch the error. If the error is raised, flow will pass to the error-handling block. This diversion may be enough to let the test pass, but if the type or message contents of the exception or error is important (such as when the error message will be shown to a user), we can use an Equality Assertion (see Assertion Method) to verify it. If the error is not raised, we call fail to report that the SUT failed to raise an error as expected.

我们应该为 SUT 可能引发的每一种异常编写一个预期异常测试。它引发错误的原因可能是客户端(即我们的测试)要求它执行某些无效操作,也可能是它转换或传递它使用的其他组件引发的错误。我们不应该为SUT 可能引发但无法强制按时发生的异常编写预期异常测试,因为这些类型的错误应该在简单成功测试中显示为测试失败。如果我们想要验证这些类型的错误是否得到正确处理,我们必须找到一种方法来强制它们发生。最常见的方法是使用测试桩第 529页)来控制 SUT 的间接输入并在测试桩中引发适当的错误。

We should write an Expected Exception Test for each kind of exception that the SUT is expected to raise. It may raise the error because the client (i.e., our test) has asked it to do something invalid, or it may translate or pass through an error raised by some other component it uses. We should not write an Expected Exception Test for exceptions that the SUT might raise but that we cannot force to occur on cue, because these kinds of errors should show up as test failures in the Simple Success Tests. If we want to verify that these kinds of errors are handled properly, we must find a way to force them to occur. The most common way to do so is to use a Test Stub (page 529) to control the indirect input of the SUT and raise the appropriate errors in the Test Stub.

异常测试在各个 xUnit 框架中有着不同的表达方式,因此编写异常测试非常有趣。JUnit 3.x 提供了一个特殊的Expected-Exception类供继承。但是,这个类迫使我们为每种测试方法(第 348页) 创建一个测试用例类,因此,与编写块相比,这实际上并没有节省任何精力,而且会产生大量非常小的测试用例类。JUnit 和 NUnit (用于 .NET) 的后续版本提供了一个特殊的方法属性(在 Java 中称为批注),用于告诉测试自动化框架,如果没有引发该异常,则测试失败。如果我们想除了异常类型之外还精确指定需要哪些文本,则此方法属性允许我们包含消息文本。try/catchExpectedException

Exception tests are very interesting to write about because of the different ways the xUnit frameworks express them. JUnit 3.x provides a special Expected-Exception class to inherit from. This class forces us to create a Testcase Class for each Test Method (page 348), however, so it really doesn't save any effort over coding a try/catch block and does result in a large number of very small Testcase Classes. Later versions of JUnit and NUnit (for .NET) provide a special ExpectedException method attribute (called an annotation in Java) to tell the Test Automation Framework to fail the test if that exception isn't raised. This method attribute allows us to include message text if we want to specify exactly which text to expect in addition to the type of the exception.

支持块的语言(例如 Smalltalk 和 Ruby)可以提供特殊断言,我们将要执行的代码块以及预期的异常/错误对象传递给这些断言。断言方法实现了确定错误是否确实发生所需的错误处理逻辑。这使我们的测试方法变得更加简单,尽管我们可能需要更仔细地检查断言的名称以查看我们拥有哪种类型的测试。

Languages that support blocks, such as Smalltalk and Ruby, can provide special assertions to which we pass the block of code to be executed as well as the expected exception/error object. The Assertion Method implements the error-handling logic required to determine whether the error has, in fact, occurred. This makes our Test Methods much simpler, even though we may need to examine the names of the assertions more closely to see which type of test we have.

变体:构造函数测试

如果我们编写的每个测试都必须验证其在夹具设置阶段创建的对象是否被正确实例化,那么我们就会有很多测试代码重复第 213页)。我们可以避免这个步骤,方法是当构造函数包含比构造函数参数中的简单字段赋值更复杂的东西时,将构造函数与其他测试方法分开测试。与在其他测试中包含构造函数逻辑验证相比,这些构造函数测试提供了更好的缺陷 定位(参见第22页)。我们可能需要为每个构造函数签名编写一个或多个测试。大多数构造函数测试都遵循简单成功测试模板;然而,我们可以使用预期异常测试来验证构造函数是否通过引发异常正确报告无效参数。

We would have a lot of Test Code Duplication (page 213) if every test we wrote had to verify that the objects it creates in its fixture setup phase are correctly instantiated. We avoid this step by testing the constructor(s) separately from other Test Methods whenever the constructor contains anything more complex than a simple field assignment from the constructor parameters. These Constructor Tests provide better Defect Localization (see page 22) than including constructor logic verification in other tests. We may need to write one or more tests for each constructor signature. Most Constructor Tests will follow a Simple Success Test template; however, we can use an Expected Exception Test to verify that the constructor correctly reports invalid arguments by raising an exception.

无论我们是否希望初始化对象或数据结构的每个属性,我们都应该验证它。对于应该初始化的属性,我们可以使用相等性断言来指定正确的值。对于不应初始化的属性,我们可以使用适合属性类型(例如,对象变量或指针)的陈述结果断言(参见断言方法)。请注意,如果我们按照每个装置一个测试用例类第 631assertNull(anObjectReference)页)来组织测试,我们可以将每个断言放入单独的测试方法中,以提供最佳的缺陷定位

We should verify each attribute of the object or data structure regardless of whether we expect it to be initialized. For attributes that should be initialized, we can use an Equality Assertion to specify the correct value. For attributes that should not be initialized, we can use a Stated Outcome Assertion (see Assertion Method) appropriate to the type of the attribute [e.g., assertNull(anObjectReference) for object variables or pointers]. Note that if we are organizing our tests with one Testcase Class per Fixture (page 631), we can put each assertion into a separate Test Method to give optimal Defect Localization.

变体:依赖初始化测试

当我们拥有一个具有可替代依赖项的对象时我们需要确保在软件投入生产时,保存对依赖组件 (DOC) 的引用的属性已初始化为真实 DOC。依赖项初始化测试是一种构造函数测试,它断言此属性已正确初始化。它通常采用与常规构造函数测试不同的测试方法来完成,以提高其可见性。

When we have an object with a substitutable dependency, we need to make sure that the attribute that holds the reference to the depended-on component (DOC) is initialized to the real DOC when the software is run in production. A Dependency Initialization Test is a Constructor Test that asserts that this attribute is initialized correctly. It is often done in a different Test Method from the normal Constructor Tests to improve its visibility.

示例:简单的成功测试

Example: Simple Success Test

下面的示例说明了一个测试,其中新手测试自动化程序已包含代码来捕获他或她知道可能发生的异常(或者测试自动化程序在调试代码时可能遇到的异常)。

The following example illustrates a test where the novice test automater has included code to catch exceptions that he or she knows might occur (or that the test automater might have encountered while debugging the code).

public void testFlightMileage_asKm() throws Exception {

      // 设置夹具

      Flight newFlight = new Flight(validFlightNumber);

      try {

            // 练习 SUT

            newFlight.setMileage(1122);

            // 验证结果

            int actualKilometres = newFlight.getMileageAsKm();

            int expectedKilometres = 1810;

            // 验证结果

            assertEquals( expectedKilometres, actualKilometres);

      } catch (InvalidArgumentException e) {

            fail(e.getMessage());

      } catch (ArrayStoreException e) {

            fail(e.getMessage());

      }

}

public  void  testFlightMileage_asKm()  throws  Exception  {

      //  set  up  fixture

      Flight  newFlight  =  new  Flight(validFlightNumber);

      try  {

            //  exercise  SUT

            newFlight.setMileage(1122);

            //  verify  results

            int  actualKilometres  =  newFlight.getMileageAsKm();

            int  expectedKilometres  =  1810;

            //  verify  results

            assertEquals(  expectedKilometres,  actualKilometres);

      }  catch  (InvalidArgumentException  e)  {

            fail(e.getMessage());

      }  catch  (ArrayStoreException  e)  {

            fail(e.getMessage());

      }

}

 

大部分代码都是不必要的,只会掩盖测试的意图。幸运的是,所有这些异常处理都可以避免。xUnit 内置了对捕获意外异常的支持。我们可以删除所有异常处理代码,让测试自动化框架捕获可能抛出的任何意外异常。意外异常被视为测试错误,因为测试以我们没有预料到的方式终止。这是有用的信息,并不比测试失败更严重。

The majority of the code is unnecessary and just obscures the intent of the test. Luckily for us, all of this exception handling can be avoided. xUnit has built-in support for catching unexpected exceptions. We can rip out all the exception-handling code and let the Test Automation Framework catch any unexpected exception that might be thrown. Unexpected exceptions are counted as test errors because the test terminates in a way we didn't anticipate. This is useful information and is not considered to be any more severe than a test failure.

public void testFlightMileage_asKm() throws Exception {

      // 设置夹具

      Flight newFlight = new Flight(validFlightNumber);

      newFlight.setMileage(1122);

      // 锻炼里程转换器

      int actualKilometres = newFlight.getMileageAsKm();

      // 验证结果

      int expectedKilometres = 1810;

     assertEquals( expectedKilometres, actualKilometres);

}

public  void  testFlightMileage_asKm()  throws  Exception  {

      //  set  up  fixture

      Flight  newFlight  =  new  Flight(validFlightNumber);

      newFlight.setMileage(1122);

      //  exercise  mileage  translator

      int  actualKilometres  =  newFlight.getMileageAsKm();

      //  verify  results

      int  expectedKilometres  =  1810;

     assertEquals(  expectedKilometres,  actualKilometres);

}

 

这个例子是 Java(一种静态类型语言),所以我们必须声明 SUT 可能会抛出异常作为测试方法签名的一部分。

This example is in Java (a statically typed language), so we had to declare that the SUT may throw an exception as part of the Test Method signature.

示例:使用 try/catch 进行预期异常测试

Example: Expected Exception Test Using try/catch

以下示例是验证异常情况的部分完成的测试。新手测试自动化人员已设置正确的测试条件,以使 SUT 引发错误。

The following example is a partially complete test to verify an exception case. The novice test automater has set up the right test condition to cause the SUT to raise an error.

public void testSetMileage_invalidInput() throws Exception {

      // 设置夹具

      Flight newFlight = new Flight(validFlightNumber);

      // 练习 SUT

      newFlight.setMileage(-1122); // 无效

      // 我们如何验证是否抛出了异常?

}

public  void  testSetMileage_invalidInput()  throws  Exception  {

      //  set  up  fixture

      Flight  newFlight  =  new  Flight(validFlightNumber);

      //  exercise  SUT

      newFlight.setMileage(-1122);    //  invalid

      //  how  do  we  verify  an  exception  was  thrown?

}

 

由于测试自动化框架会捕获异常并使测试失败,因此即使 SUT 的行为正确,测试运行器也不会显示绿色条。我们可以在测试的练习阶段引入一个错误处理块,并使用它来反转通过/失败标准(抛出异常时通过;未抛出异常时失败)。以下是如何在 JUnit 3.x 中验证 SUT 是否按预期失败:

Because the Test Automation Framework will catch the exception and fail the test, the Test Runner will not exhibit the green bar even though the SUT's behavior is correct. We can introduce an error-handling block around the exercise phase of the test and use it to invert the pass/fail criteria (pass when the exception is thrown; fail when it is not). Here's how to verify that the SUT fails as expected in JUnit 3.x:

public void testSetMileage_invalidInput() throws Exception {

      // 设置夹具

      Flight newFlight = new Flight(validFlightNumber);

      try {

            // 练习 SUT

            newFlight.setMileage(-1122);

            fail("应该抛出 InvalidInputException");

      } catch( InvalidArgumentException e) {

            // 验证结果

            assertEquals( "飞行里程必须为正数",

                              e.getMessage());

      }

}

public  void  testSetMileage_invalidInput()  throws  Exception  {

      //  set  up  fixture

      Flight  newFlight  =  new  Flight(validFlightNumber);

      try  {

            //  exercise  SUT

            newFlight.setMileage(-1122);

            fail("Should  have  thrown  InvalidInputException");

      }  catch(  InvalidArgumentException  e)  {

            //  verify  results

            assertEquals(  "Flight  mileage  must  be  positive",

                              e.getMessage());

      }

}

 

这种样式try/catch只能用于允许我们精确指定要捕获的异常的语言。如果我们想捕获断言方法exception抛出的一般异常或相同异常,它将不起作用,因为这些异常将使我们陷入子句。在这些情况下,我们需要使用与自定义断言测试中使用的相同样式的预期异常测试(第474页)。 failcatch

This style of try/catch can be used only in languages that allow us to specify exactly which exception to catch. It won't work if we want to catch a generic exception or the same exception that the Assertion Method fail throws, because these exceptions will send us into the catch clause. In these cases we need to use the same style of Expected Exception Test as used in tests of Custom Assertions (page 474).

public void testSetMileage_invalidInput2() throws Exception {

      // 设置夹具

      Flight newFlight = new Flight(validFlightNumber);

      try {

            // 练习 SUT

            newFlight.setMileage(-1122);

            // 如果 SUT 抛出相同类型的异常,则此处不能失败()

      } catch( AssertionFailedError e) {

            // 验证结果

            assertEquals( "飞行里程必须为正数",

                               e.getMessage());

            return;

      }

      fail("应该抛出 InvalidInputException");

}

public  void  testSetMileage_invalidInput2()  throws  Exception  {

      //  set  up  fixture

      Flight  newFlight  =  new  Flight(validFlightNumber);

      try  {

            //  exercise  SUT

            newFlight.setMileage(-1122);

            //  cannot  fail()  here  if  SUT  throws  same  kind  of  exception

      }  catch(  AssertionFailedError  e)  {

            //  verify  results

            assertEquals(  "Flight  mileage  must  be  positive",

                               e.getMessage());

            return;

      }

      fail("Should  have  thrown  InvalidInputException");

}

 

示例:使用方法属性进行预期异常测试

Example: Expected Exception Test Using Method Attributes

NUnit 提供了一种方法属性,让我们可以编写预期异常测试,而无需强迫我们明确地编写try/catch块代码。

NUnit provides a method attribute that lets us write an Expected Exception Test without forcing us to code a try/catch block explicitly.

[测试]

[ExpectedException(typeof( InvalidArgumentException),

                                        "飞行里程必须 > 零")]

public void testSetMileage_invalidInput_AttributeWithMessage()

{

      // 设置夹具

      Flight newFlight = new Flight(validFlightNumber);

      // 练习 SUT

      newFlight.setMileage(-1122);

}

[Test]

[ExpectedException(typeof(  InvalidArgumentException),

                                        "Flight  mileage  must  be  >  zero")]

public  void  testSetMileage_invalidInput_AttributeWithMessage()

{

      //  set  up  fixture

      Flight  newFlight  =  new  Flight(validFlightNumber);

      //  exercise  SUT

      newFlight.setMileage(-1122);

}

 

这种方法确实使测试更加紧凑,但除了异常类型或异常所含消息外,没有提供指定任何其他内容的方法。如果我们想对异常的其他内容做出任何断言(以避免敏感平等请参阅第239页的脆弱测试),我们需要使用try/catch

This approach does make the test much more compact but doesn't provide a way to specify anything but the type of the exception or the message it contains. If we want to make any assertions on other contents of the exception (to avoid Sensitive Equality; see Fragile Test on page 239), we'll need to use try/catch.

示例:使用块闭包进行预期异常测试

Example: Expected Exception Test Using Block Closure

Smalltalk 的 SUnit 提供了另一种机制来实现同样的目的:

Smalltalk's SUnit provides another mechanism to achieve the same thing:

testSetMileageWithInvalidInput

      self

          should: [飞行新里程:-1122]

          raise: RuntimeError new '应该引发错误'

testSetMileageWithInvalidInput

      self

          should:  [Flight  new  mileage:  -1122]

          raise:  RuntimeError  new  'Should  have  raised  error'

 

因为 Smalltalk 支持块闭包,所以我们将要执行的代码块should:raise:与预期的Exception对象一起传递给方法。Ruby 的 Test::Unit 使用相同的方法:

Because Smalltalk supports block closures, we pass the block of code to be executed to the method should:raise: along with the expected Exception object. Ruby's Test::Unit uses the same approach:

def testSetMileage_invalidInput

      flight = Flight.new();

      assert_raises( RuntimeError, "应该引发错误") do

            flight.setMileage(-1122)

      end

end

def  testSetMileage_invalidInput

      flight  =  Flight.new();

      assert_raises(  RuntimeError,  "Should  have  raised  error")  do

            flight.setMileage(-1122)

      end

end

 

这对之间的代码do/end是方法执行的闭包assert_raises。如果它没有引发第一个参数(类RuntimeError)的实例,则测试失败并显示提供的错误消息。

The code between the do/end pair is a closure that is executed by the assert_raises method. If it doesn't raise an instance of the first argument (the class RuntimeError), the test fails and presents the error message supplied.

示例:构造函数测试

Example: Constructor Test

在此示例中,我们需要构建一个航班来测试飞行距离从英里到公里的转换。首先,我们将确保航班构建正确。

In this example, we need to build a flight to test the conversion of the flight distance from miles to kilometers. First, we'll make sure the flight is constructed properly.

public void testFlightMileage_asKm2() throws Exception {

      // 设置夹具

      // 练习构造函数

      Flight newFlight = new Flight(validFlightNumber);

      // 验证构造的对象

     assertEquals(validFlightNumber, newFlight.number);

     assertEquals("", newFlight.airlineCode);

      assertNull(newFlight.airline);

      // 设置里程

      newFlight.setMileage(1122);

      // 练习里程转换器

      int actualKilometres = newFlight.getMileageAsKm();

      // 验证结果

      int expectedKilometres = 1810;

     assertEquals( expectedKilometres, actualKilometres);

      // 现在用取消的航班尝试

      newFlight.cancel();

      try {

            newFlight.getMileageAsKm();

            fail("Expected exception");

      } catch (InvalidRequestException e) {

            assertEquals( "无法获取取消的航班里程",

                            e.getMessage());

      }

}

public  void  testFlightMileage_asKm2()  throws  Exception  {

      //  set  up  fixture

      //  exercise  constructor

      Flight  newFlight  =  new  Flight(validFlightNumber);

      //  verify  constructed  object

     assertEquals(validFlightNumber,  newFlight.number);

     assertEquals("",  newFlight.airlineCode);

      assertNull(newFlight.airline);

      //  set  up  mileage

      newFlight.setMileage(1122);

      //  exercise  mileage  translator

      int  actualKilometres  =  newFlight.getMileageAsKm();

      //  verify  results

      int  expectedKilometres  =  1810;

     assertEquals(  expectedKilometres,  actualKilometres);

      //  now  try  it  with  a  canceled  flight

      newFlight.cancel();

      try  {

            newFlight.getMileageAsKm();

            fail("Expected  exception");

      }  catch  (InvalidRequestException  e)  {

            assertEquals(  "Cannot  get  cancelled  flight  mileage",

                            e.getMessage());

      }

}

 

此测试不是单条件测试(见第45页),因为它检查对象构造和距离转换行为。如果对象构造失败,我们将无法知道哪个问题导致了失败,直到我们开始调试测试。

This test is not a Single-Condition Test (see page 45) because it examines both object construction and distance conversion behavior. If object construction fails, we won't know which issue was the cause of the failure until we start debugging the test.

最好将此Eager Test(请参阅第 224页的断言轮盘)分成两个测试,每个测试都是一个单一条件测试。最简单的方法是克隆测试方法,重命名每个副本以反映它在单一条件测试中会做什么,然后删除任何不满足该目标的代码。

It would be better to separate this Eager Test (see Assertion Roulette on page 224) into two tests, each of which is a Single-Condition Test. This is most easily done by cloning the Test Method, renaming each copy to reflect what it would do if it were a Single-Condition Test, and then removing any code that doesn't satisfy that goal.

这是一个简单的构造函数测试的示例:

Here's an example of a simple Constructor Test:

public void testFlightConstructor_OK() throws Exception {

      // 设置夹具

      // 练习 SUT

      Flight newFlight = new Flight(validFlightNumber);

      // 验证结果

     assertEquals( validFlightNumber, newFlight.number );

     assertEquals( "", newFlight.airlineCode );

      assertNull( newFlight.airline );

}

public  void  testFlightConstructor_OK()  throws  Exception  {

      //  set  up  fixture

      //  exercise  SUT

      Flight  newFlight  =  new  Flight(validFlightNumber);

      //  verify  results

     assertEquals(  validFlightNumber,  newFlight.number  );

     assertEquals(  "",  newFlight.airlineCode  );

      assertNull(  newFlight.airline  );

}

 

在此过程中,我们不妨通过使用构造函数测试的预期异常测试模板来指定如果将无效参数传递给构造函数应该发生什么:

While we are at it, we might as well specify what should occur if an invalid argument is passed to the constructor by using the Expected Exception Test template for our Constructor Test:

public void testFlightConstructor_badInput() {

      // 设置夹具

      BigDecimal invalidFlightNumber = new BigDecimal(-1023);

      // 练习 SUT

      try {

            Flight newFlight = new Flight(invalidFlightNumber);

            fail("未捕获负航班号!");

      } catch (InvalidArgumentException e) {

            // 验证结果

            assertEquals( "航班号必须为正数",

                             e.getMessage());

      }

}

public  void  testFlightConstructor_badInput()  {

      //  set  up  fixture

      BigDecimal  invalidFlightNumber  =  new  BigDecimal(-1023);

      //  exercise  SUT

      try  {

            Flight  newFlight  =  new  Flight(invalidFlightNumber);

            fail("Didn't  catch  negative  flight  number!");

      }  catch  (InvalidArgumentException  e)  {

            //  verify  results

            assertEquals(  "Flight  numbers  must  be  positive",

                             e.getMessage());

      }

}

 

现在我们知道我们的构造函数逻辑已经过充分测试,我们就可以编写里程翻译功能的简单成功测试了。请注意,它变得多么简单,因为我们可以专注于验证业务逻辑:

Now that we know that our constructor logic is well tested, we are ready to write our Simple Success Test for our mileage translation functionality. Note how much simpler it has become because we can focus on verifying the business logic:

public void testFlightMileage_asKm() throws Exception {

      // 设置夹具

      Flight newFlight = new Flight(validFlightNumber);

      newFlight.setMileage(1122);

      // 锻炼里程转换器

      int actualKilometres = newFlight.getMileageAsKm();

      // 验证结果

      int expectedKilometres = 1810;

     assertEquals( expectedKilometres, actualKilometres);

}

public  void  testFlightMileage_asKm()  throws  Exception  {

      //  set  up  fixture

      Flight  newFlight  =  new  Flight(validFlightNumber);

      newFlight.setMileage(1122);

      //  exercise  mileage  translator

      int  actualKilometres  =  newFlight.getMileageAsKm();

      //  verify  results

      int  expectedKilometres  =  1810;

     assertEquals(  expectedKilometres,  actualKilometres);

}

 

那么,如果构造函数逻辑缺陷,会发生什么情况呢?这个测试很可能会失败,因为它的输出取决于传递给构造函数的值。构造函数测试也会失败。这个失败会告诉我们首先要查看构造函数逻辑。一旦修复了这个问题,这个测试很可能会通过。如果没有通过,那么我们可以专注于修复方法逻辑。这是缺陷定位getMileageAsKm的一个很好的例子。

So what happens if the constructor logic is defective? This test will likely fail because its output depends on the value passed to the constructor. The constructor test will also fail. That failure will tell us to look at the constructor logic first. Once that problem is fixed, this test will likely pass. If it doesn't, then we can focus on fixing the getMileageAsKm method logic. This is a good example of Defect Localization.

四相测试

Four-Phase Test

我们如何构建我们的测试逻辑来使我们正在测试的内容显而易见?

How do we structure our test logic to make what we are testing obvious?

我们将每个测试构建为按顺序执行的四个不同部分。

We structure each test with four distinct parts executed in sequence.

图像

工作原理

How It Works

我们将每个测试设计为有四个按顺序执行的不同阶段:固定装置设置、练习 SUT、结果验证和固定装置拆卸。

We design each test to have four distinct phases that are executed in sequence: fixture setup, exercise SUT, result verification, and fixture teardown.

  • 在第一阶段,我们设置测试装置(“之前”的图片),这是 SUT 表现出预期行为所必需的,以及您需要放置到位的任何东西,以便能够观察实际结果(例如,使用测试替身;参见第 522页)。
  • In the first phase, we set up the test fixture (the "before" picture) that is required for the SUT to exhibit the expected behavior as well as anything you need to put in place to be able to observe the actual outcome (such as using a Test Double; see page 522).
  • 在第二阶段,我们与 SUT 进行交互。
  • In the second phase, we interact with the SUT.
  • 在第三阶段,我们会尽一切必要的努力来确定是否已经获得了预期的结果。
  • In the third phase, we do whatever is necessary to determine whether the expected outcome has been obtained.
  • 在第四阶段,我们拆除测试装置,让世界回到我们发现时的状态。
  • In the fourth phase, we tear down the test fixture to put the world back into the state in which we found it.

我们为什么这样做

Why We Do This

测试阅读者必须能够快速确定测试正在验证哪些行为。当调用 SUT 的各种行为时,可能会非常混乱 — 有些行为用于设置 SUT 的测试前状态(夹具),有些行为用于锻炼 SUT,还有一些行为用于验证 SUT 的测试后状态。清楚地识别这四个阶段可以更清楚地了解测试的意图。

The test reader must be able to quickly determine what behavior the test is verifying. It can be very confusing when various behaviors of the SUT are being invoked—some to set up the pre-test state (fixture) of the SUT, others to exercise the SUT, and yet others to verify the post-test state of the SUT. Clearly identifying the four phases makes the intent of the test much easier to see.

测试的夹具设置阶段在测试之前确定 SUT 的状态,这是测试的重要输入。练习 SUT 阶段是我们实际运行正在测试的软件的地方。在阅读测试时,我们需要查看正在运行哪个软件。测试的结果验证阶段是我们指定预期结果的地方。最后一个阶段,夹具拆卸,完全是内务管理。我们不想用它来掩盖重要的测试逻辑,因为从测试即文档的角度来看,它是完全不相关的参见第23页)。

The fixture setup phase of the test establishes the SUT's state prior to the test, which is an important input to the test. The exercise SUT phase is where we actually run the software we are testing. When reading the test, we need to see which software is being run. The result verification phase of the test is where we specify the expected outcome. The final phase, fixture teardown, is all about housekeeping. We wouldn't want to obscure the important test logic with it because it is completely irrelevant from the perspective of Tests as Documentation (see page 23).

我们应该避免在单个测试方法(第 348页)中测试尽可能多的功能的诱惑,因为这样会导致模糊测试(第 186页)。事实上,最好有许多小型的单条件测试(参见第45页)。使用注释来标记四阶段测试的各个阶段是一种很好的自律来源,因为它可以很清楚地表明我们的测试何时不是单条件测试。如果我们有多个由结果验证阶段分隔的练习 SUT 阶段,或者我们有穿插的夹具设置和练习 SUT 阶段,这将是不言而喻的。当然,这些测试可能会起作用——但与我们有一堆独立的单条件测试相比,它们提供的缺陷定位(参见第 22页) 较少。

We should avoid the temptation to test as much functionality as possible in a single Test Method (page 348) because that can result in Obscure Tests (page 186). In fact, it is preferable to have many small Single-Condition Tests (see page 45). Using comments to mark the phases of a Four-Phase Test is a good source of self-discipline, in that it makes it very obvious when our tests are not Single-Condition Tests. It will be self-evident if we have multiple exercise SUT phases separated by result verification phases or if we have interspersed fixture setup and exercise SUT phases. Sure, the tests may work—but they will provide less Defect Localization (see page 22) than if we have a bunch of independent Single-Condition Tests.

实施说明

Implementation Notes

我们有几种实现四阶段测试的选项。在最简单的情况下,每个测试都是完全独立的。测试的所有四个阶段都包含在测试方法的主体中。这种结构意味着我们使用内联设置第 408页)和垃圾收集拆卸第 500页)或内联拆卸(第509页)。当我们使用每个类的测试用例类第 617页)或每个功能的测试用例类第 624页)来组织我们的测试方法时,这是最合适的选择。

We have several options for implementing the Four-Phase Test. In the simplest case, each test is completely free-standing. All four phases of the test are contained within the body of the Test Method. This structure implies we are using In-line Setup (page 408) and either Garbage-Collected Teardown (page 500) or In-line Teardown (page 509). It is the most appropriate choice when we are using Testcase Class per Class (page 617) or Testcase Class per Feature (page 624) to organize our Test Methods.

另一种选择是利用测试自动化框架(第298页) 对隐式设置(第 424页) 和隐式拆卸(第516页) 的支持。我们将通用的夹具设置和夹具拆卸逻辑分解到测试用例类(第 373setUp页) 上的和tearDown方法中。这样,测试方法中就只剩下练习 SUT 和结果验证阶段。当我们使用每个夹具的测试用例类(第 631页) 时,这种方法是一种合适的选择。当使用每个类的测试用例类(第 617页) 或每个功能的测试用例类时,我们还可以使用此方法来设置夹具的通用部分,或者在使用自动拆卸(第503页)时拆卸夹具。

The other choice is to take advantage of the Test Automation Framework's (page 298) support for Implicit Setup (page 424) and Implicit Teardown (page 516). We factor out the common fixture setup and fixture teardown logic into setUp and tearDown methods on the Testcase Class (page 373). This leaves only the exercise SUT and result verification phases in the Test Method. This approach is an appropriate choice when we are using Testcase Class per Fixture (page 631). We can also use this approach to set up common parts of the fixture when using Testcase Class per Class (page 617) or Testcase Class per Feature or to tear down the fixture when using Automated Teardown (page 503).

示例:四相测试(在线)

Example: Four-Phase Test (In-line)

这是一个明显是四阶段测试的测试示例:

Here is an example of a test that is clearly a Four-Phase Test:

public void testGetFlightsByOriginAirport_NoFlights_inline()

            throws Exception {

      // Fixture 设置

      NonTxFlightMngtFacade Facade =new NonTxFlightMngtFacade();

      BigDecimal airportId = Facade.createTestAirport("1OF");

      try {

            // 练习系统

            列表 flightsAtDestination1 =

                            Facade.getFlightsByOriginAirport(airportId);

            // 验证结果

            assertEquals( 0, flightsAtDestination1.size() );

      } finally {

            // Fixture 拆除

            Facade.removeAirport( airportId );

      }

}

public  void  testGetFlightsByOriginAirport_NoFlights_inline()

            throws  Exception  {

      //  Fixture  setup

      NonTxFlightMngtFacade  facade  =new  NonTxFlightMngtFacade();

      BigDecimal  airportId  =  facade.createTestAirport("1OF");

      try  {

            //  Exercise  system

            List  flightsAtDestination1  =

                            facade.getFlightsByOriginAirport(airportId);

            //  Verify  outcome

            assertEquals(  0,  flightsAtDestination1.size()  );

      }  finally  {

            //  Fixture  teardown

            facade.removeAirport(  airportId  );

      }

}

 

四阶段测试的所有四个阶段都包含在内联代码中。由于对断言方法(第 362页) 的调用会引发异常,因此我们需要用构造包围测试方法的夹具拆卸部分,try/finally以确保它在所有情况下都运行。

All four phases of the Four-Phase Test are included as in-line code. Because the calls to Assertion Methods (page 362) raise exceptions, we need to surround the fixture teardown part of the Test Method with a try/finally construct to ensure that it is run in all cases.

示例:四阶段测试(隐式设置/拆卸)

Example: Four-Phase Test (Implicit Setup/Teardown)

以下是相同的四阶段测试,其中夹具设置和夹具拆卸逻辑已从测试方法中移出:

Here is the same Four-Phase Test with the fixture setup and fixture teardown logic moved out of the Test Method:

NonTxFlightMngtFacade Facade = new NonTxFlightMngtFacade();

private BigDecimal airportId;



protected void setUp() throws Exception {

      // Fixture 设置

      super.setUp();

      airportId = Facade.createTestAirport("1OF");

}

public void testGetFlightsByOriginAirport_NoFlights_implicit()

            throws Exception {

      // 练习 SUT

      列表 flightsAtDestination1 =

               Facade.getFlightsByOriginAirport(airportId);

      // 验证结果

     assertEquals( 0, flightsAtDestination1.size() );

}

protected void teaDown() throws Exception {

      // Fixture 拆卸

      Facade.removeAirport(airportId);

      super.tearDown();

}

NonTxFlightMngtFacade  facade  =  new  NonTxFlightMngtFacade();

private  BigDecimal  airportId;



protected  void  setUp()  throws  Exception  {

      //  Fixture  setup

      super.setUp();

      airportId  =  facade.createTestAirport("1OF");

}

public  void  testGetFlightsByOriginAirport_NoFlights_implicit()

            throws  Exception  {

      //  Exercise  SUT

      List  flightsAtDestination1  =

               facade.getFlightsByOriginAirport(airportId);

      //  Verify  outcome

     assertEquals(  0,  flightsAtDestination1.size()  );

}

protected  void  tearDown()  throws  Exception  {

      //  Fixture  teardown

      facade.removeAirport(airportId);

      super.tearDown();

}

 

因为该tearDown方法即使在测试失败后也会自动调用,所以我们不需要在测试方法try/finally中构造。然而,缺点是,对我们的装置的引用必须保存在实例变量中,而不是局部变量中。

Because the tearDown method is called automatically even after test failures, we don't need the try/finally construct inside the Test Method. The downside, however, is that references to our fixture must be held in instance variables rather than local variables.

断言方法

Assertion Method

我们如何使测试自我检查?

How do we make tests self-checking?

我们调用效用方法来评估是否实现了预期结果。

We call a utility method to evaluate whether an expected outcome has been achieved.

图像

编写全自动测试(见第26页)的一个关键部分是使它们成为自检测试(见第26页),以避免每次运行时都必须检查每个测试的结果是否正确。此策略涉及找到一种表达预期结果的方法,以便测试本身可以自动验证它。

A key part of writing Fully Automated Tests (see page 26) is to make them Self-Checking Tests (see page 26) to avoid having to inspect the outcome of each test for correctness each time it is run. This strategy involves finding a way to express the expected outcome so that it can be verified automatically by the test itself.

断言方法为我们提供了一种表达预期结果的方式,这种方式既可由计算机执行,又对人类读者有用,然后人类读者可以将测试用作文档(参见第23)。

Assertion Methods give us a way to express the expected outcome in a way that is both executable by the computer and useful to the human reader, who can then use the Tests as Documentation (see page 23).

工作原理

How It Works

我们将测试的预期结果编码为一系列断言,这些断言说明了测试通过时应满足哪些条件。这些断言通过调用断言方法来实现,这些方法封装了导致测试失败的机制。断言方法可以由测试自动化框架(第 298页)提供,也可以由测试自动化程序以自定义断言(第 474页) 的形式提供。

We encode the expected outcome of the test as a series of assertions that state what should be true for the test to pass. The assertions are realized as calls to Assertion Methods that encapsulate the mechanism that causes the test to fail. The Assertion Methods may be provided by the Test Automation Framework (page 298) or by the test automater as Custom Assertions (page 474).

我们为什么这样做

Why We Do This

使用条件测试逻辑(第 200页)对预期结果进行编码非常冗长,使测试难以阅读和理解。它也更有可能导致测试代码重复(第 213页) 和错误测试(第 260页)。断言方法通过将复杂性转移到可重用的测试实用程序方法(第 599页) 来帮助我们避免这些问题;然后可以使用测试实用程序测试来验证这些方法是否正常工作(请参阅测试实用程序方法)。

Encoding the expected outcome using Conditional Test Logic (page 200) is very verbose and makes tests hard to read and understand. It is also much more likely to lead to Test Code Duplication (page 213) and Buggy Tests (page 260). Assertion Methods help us avoid these issues by moving that complexity into reusable Test Utility Methods (page 599); these methods can then be verified as working correctly using Test Utility Tests (see Test Utility Method).

实施说明

Implementation Notes

尽管 xUnit 系列的所有成员都提供了断言方法,但它们的实现方式具有相当大的可变性。关键的实现注意事项如下:

Although all members of the xUnit family provide Assertion Methods, they do so with a fair degree of variability. The key implementation considerations are as follows:

调用内置断言方法

在测试方法(第 348页)中调用断言方法的方式因语言和框架而异。语言特性决定了什么是可能的和可取的,而框架构建者则选择使用哪些选项。这些开发人员为断言方法选择的名称受到他们选择如何访问它们的影响。以下是访问断言方法的最常见选项:

The way the Assertion Methods are called from within the Test Method (page 348) varies from language to language and from framework to framework. The language features determine what is possible and preferable, while the framework builders chose which options to use. The names these developers chose for the Assertion Methods were influenced by how they chose to access them. Here are the most common options for accessing the Assertion Methods:

  • 断言方法继承自框架提供的测试用例超类(第 638页)。可以像在测试用例类(第373页) 本地提供一样调用这些方法。例如,Java 的 JUnit 的原始版本就使用了这种方法,它提供了一个从类继承的测试用例超类Assert,其中包含实际的断言方法
  • The Assertion Methods are inherited from a Testcase Superclass (page 638) provided by the framework. Such methods may be invoked as though they were provided locally on the Testcase Class (page 373). The original version of Java's JUnit, for example, used this approach by providing a Testcase Superclass that inherits from the class Assert, which contains the actual Assertion Methods.
  • 断言方法通过全局可访问的类或模块提供。使用类或模块名称来调用它们以完全限定断言方法名称。例如,NUnit 使用此方法 [例如Assert.isTrue(x);]。JUnit 确实允许将断言作为Assert类上的静态方法调用 [例如Assert.assertTrue(x)],但这通常不是必需的,因为它们是通过测试用例超类继承的。
  • The Assertion Methods are provided via a globally accessible class or module. They are invoked using the class or module name to fully qualify the Assertion Method name. NUnit, for example, uses this approach [e.g., Assert.isTrue(x);]. JUnit does allow assertions to be invoked as static methods on the Assert class [e.g., Assert.assertTrue(x)] but this is not usually necessary because they are inherited via the Testcase Superclass.
  • 断言方法以“混合”或宏的形式提供。例如,Ruby 的 Test::Unit在名为 的模块中提供了断言方法,Assert该模块可包含在任何类中,2从而允许使用断言方法,就像在Testcase 类中定义一样[例如assert_equal(a,b)]。相比之下,CppUnit 将断言方法定义为宏,这些宏在编译器看到代码之前就会展开。
  • The Assertion Methods are provided as "mix-ins" or macros. Ruby's Test::Unit, for example, provides the Assertion Methods in a module called Assert that can be included in any class,2 thereby allowing the Assertion Methods to be used as though defined within the Testcase Class [e.g., assert_equal(a,b)]. CppUnit, by contrast, defines the Assertion Methods as macros, which are expanded before the compiler sees the code.
断言消息

断言方法通常将可选的断言消息作为文本参数,当断言失败时,该文本参数将包含在输出中。这种结构允许测试自动化程序向测试维护人员解释哪个断言方法失败了,并更好地解释应该发生什么。如果断言方法提供了有关失败原因的更多信息,则测试检测到的错误将更容易调试。选择正确的断言方法对于实现这一目标大有帮助,因为许多内置断言方法提供了有关参数值的有用诊断信息。对于相等性断言尤其如此。

Assertion Methods typically take an optional Assertion Message as a text parameter that is included in the output when the assertion fails. This structure allows the test automater to explain to the test maintainer exactly which Assertion Method failed and to better explain what should have occurred. The error detected by the test will be much easier to debug if the Assertion Method provides more information about why it failed. Choosing the right Assertion Method goes a long way toward achieving this goal because many of the built-in Assertion Methods provide useful diagnostic information about the values of the arguments. This is especially true for Equality Assertions.

xUnit 系列成员之间的最大区别之一是可选断言消息在参数列表中出现的位置。大多数成员将其作为可选参数添加到末尾。但是,JUnit 会将断言消息作为第一个参数(如果存在)。

One of the biggest differences between members of the xUnit family is where the optional Assertion Message appears in the argument list. Most members tack it on to the end as an optional argument. JUnit, however, makes the Assertion Message the first argument when it is present.

选择正确的断言

我们对测试方法中断言方法的调用有两个目标:

We have two goals for the calls to Assertion Methods in our Test Methods:

  • 当发生与预期不同的结果时,测试失败
  • Fail the test when something other than the expected outcome occurs
  • 记录 SUT应该如何运行(即测试作为文档
  • Document how the SUT is supposed to behave (i.e., Tests as Documentation)

为了实现这些目标,我们必须努力使用最合适的断言方法虽然 xUnit 系列中各个成员的语法和命名约定各不相同,但大多数都提供了一组基本断言,这些断言分为以下几类:

To achieve these goals we must strive to use the most appropriate Assertion Method. While the syntax and naming conventions vary from one member of the xUnit family to the next, most provide a basic set of assertions that fall into the following categories:

  • 单一结果断言,fail;这些断言不需要任何参数,因为它们总是以相同的方式运行。
  • Single-Outcome Assertions such as fail; these take no arguments because they always behave the same way.
  • 陈述结果断言例如assertNotNull(anObjectReference)assertTrue(aBooleanExpression);它们将单个参数与方法名称暗示的结果进行比较。
  • Stated Outcome Assertions such as assertNotNull(anObjectReference) and assertTrue(aBooleanExpression); these compare a single argument to an outcome implied by the method name.
  • 诸如此类的预期异常断言assert_raises(expectedError)  {  codeToExecute  };会评估一段代码和一个单一的预期异常参数。
  • Expected Exception Assertions such as assert_raises(expectedError)  {  codeToExecute  }; these evaluate a block of code and a single expected exception argument.
  • 平等断言,例如assertEqual(expected,  actual);它们比较两个对象或值是否平等。
  • Equality Assertions such as assertEqual(expected,  actual); these compare two objects or values for equality.
  • 模糊平等断言,例如assertEqual(expected,  actual,  tolerance);它们通过使用“容差”或“比较掩码”来确定两个值是否“足够接近”。
  • Fuzzy Equality Assertions such as assertEqual(expected,  actual,  tolerance); these determine whether two values are "close enough" to each other by using a "tolerance" or "comparison mask."
变体:平等断言

相等性断言是断言方法最常见的例子。它们用于将实际结果与以常量文字值(第 714页) 或预期对象(请参阅第 462页的状态验证)形式表示的预期结果进行比较。按照惯例,首先指定预期值,然后指定实际值。测试自动化框架生成的诊断消息只有按此顺序提供才有意义。两个对象是否相等通常通过调用预期对象上的方法。如果 SUT 的定义不是我们想要在测试中使用的,我们可以在对象的各个字段上进行相等性断言,也可以在预期对象的测试特定子类(第 579页)上实现特定于测试的相等性equalsequals

Equality Assertions are the most common examples of Assertion Methods. They are used to compare the actual outcome with an expected outcome that is expressed in the form of a constant Literal Value (page 714) or an Expected Object (see State Verification on page 462). By convention, the expected value is specified first and the actual value follows it. The diagnostic message that is generated by the Test Automation Framework makes sense only when they are provided in this order. The equality of the two objects is usually determined by invoking the equals method on the expected object. If the SUT's definition of equals is not what we want to use in our tests, either we can make Equality Assertions on individual fields of the object or we can implement our test-specific equality on a Test-Specific Subclass (page 579) of the Expected Object.

变体:模糊相等断言

当我们由于精度变化或预期值变化而无法保证完全匹配时,使用模糊相等断言可能是合适的。通常,这些断言看起来就像相等断言,但增加了一个额外的“容差”或“比较图”参数,该参数指定实际参数必须与预期参数有多接近。模糊相等断言最常见的例子是浮点数的比较,其中需要通过提供容差(两个值之间可接受的最大距离)来考虑算术精度的限制。

When we cannot guarantee an exact match due to variations in precision or expected variations in value, it may be appropriate to use a Fuzzy Equality Assertion. Typically, these assertions look just like Equality Assertions with the addition of an extra "tolerance" or "comparison map" parameter that specifies how close the actual argument must be to the expected one. The most common example of a Fuzzy Equality Assertion is the comparison of floating-point numbers where the limitations of arithmetic precision need to be accounted for by providing a tolerance (the maximum acceptable distance between the two values).

在比较 XML 文档时,我们使用相同的方法,因为直接比较字符串可能会由于某些字段具有不可预测的内容而导致失败。在这种情况下,“模糊”规范是一种“比较模式”,它指定哪些字段需要匹配或哪些字段应该被忽略。这种相等断言与断言字符串符合正则表达式或其他形式的模式匹配非常相似。

We use the same approach when comparing XML documents where direct string comparisons may result in failure owing to certain fields having unpredictable content. In this case, the "fuzz" specification is a "comparison schema" that specifies which fields need to match or which fields should be ignored. This kind of Equality Assertion is very similar to asserting that a string conforms to a regular expression or other form of pattern matching.

变体:陈述结果断言

陈述结果断言是一种无需传递预期值作为参数即可准确说明结果的方式。结果必须足够常见才能保证使用特殊的断言方法。最常见的示例如下:

Stated Outcome Assertions are a way of saying exactly what the outcome should be without passing an expected value as an argument. The outcome must be common enough to warrant a special Assertion Method. The most common examples are as follows:

  • assertTrue(aBooleanExpression),如果表达式计算结果为 FALSE,则失败
  • assertTrue(aBooleanExpression), which fails if the expression evaluates to FALSE
  • assertNotNull(anObjectReference)objectReference,如果没有引用有效的对象,则失败
  • assertNotNull(anObjectReference), which fails if the objectReference doesn't refer to a valid object

陈述结果断言通常用作保护断言第 490页),以避免条件测试逻辑

Stated Outcome Assertions are often used as Guard Assertions (page 490) to avoid Conditional Test Logic.

变体:预期异常断言

在支持块闭包的语言中我们可以使用声明结果断言的变体,该断言采用一个附加参数来指定我们期望的异常类型。我们可以使用此预期异常断言来表示“运行此块并验证是否抛出了以下异常”。这种格式比使用try/catch构造更紧凑。以下是一些典型示例:

In languages that support block closures, we can use a variation of a Stated Outcome Assertion that takes an additional parameter specifying the kind of exception we expect. We can use this Expected Exception Assertion to say, "Run this block and verify that the following exception is thrown." This format is more compact than using a try/catch construct. Some typical examples follow:

  • should:  [aBlockToExecute]  raise:  expectedException在 Smalltalk 的 SUnit 中
  • should:  [aBlockToExecute]  raise:  expectedException in Smalltalk's SUnit
  • assert_raises(  expectedError)  {  codeToExecute  }在 Ruby 的 Test::Unit 中
  • assert_raises(  expectedError)  {  codeToExecute  } in Ruby's Test::Unit
变体:单一结果断言

单一结果断言总是以相同的方式运行。最常用的单一结果断言fail,它导致测试被视为失败。它通常在两种情况下使用:

A Single-Outcome Assertion always behaves the same way. The most commonly used Single-Outcome Assertion is fail, which causes a test to be treated as a failure. It is typically used in two circumstances:

  • 当测试首次被识别并作为几乎为空的测试方法执行时,作为未完成的测试断言第 494页) 。通过包含对的调用,我们可以让测试运行器第 377页)提醒我们还有一个测试需要完成编写。fail
  • As an Unfinished Test Assertion (page 494) when a test is first identified and implemented as a nearly empty Test Method. By including a call to fail, we can have the Test Runner (page 377) remind us that we still have a test to finish writing.
  • 作为预期异常测试(参见测试方法try/catch)中块(或等效块)的一部分,在预期会引发异常的调用之后立即在块中包含对的调用。如果我们不想对捕获的异常进行断言,我们可以通过使用单一结果断言来记录这是预期结果,从而避免出现空的 catch 块。failtry success
  • As part of a try/catch (or equivalent) block in an Expected Exception Test (see Test Method) by including a call to fail in the try block immediately after the call that is expected to throw an exception. If we don't want to assert something about the exception that was caught, we can avoid an empty catch block by using the Single-Outcome Assertion success to document that this is the expected outcome.

我们确实不应该使用单一结果断言的一种情况是条件测试逻辑。几乎没有理由在测试方法中包含条件逻辑,因为通常有使用其他样式的断言方法的更具声明性的方式来处理这种情况。例如,使用Guard 断言会使测试更容易理解,并且不太可能产生不正确的结果。

One circumstance in which we really should not use Single-Outcome Assertions is in Conditional Test Logic. There is almost never a good reason to include conditional logic in a Test Method, as there is usually a more declarative way to handle this situation using other styles of Assertion Methods. For example, use of Guard Assertions results in tests that are more easily understood and less likely to yield incorrect results.

激励人心的例子

Motivating Example

下面的例子说明了如果没有断言方法,我们想要验证的每个项目都需要什么样的代码。我们真正想要做的就是这些:

The following example illustrates the kind of code that would be required for each item we wanted to verify if we did not have Assertion Methods. All we really want to do is this:

if (x.equals(y)) {

    抛出新的 AssertionFailedError(

                “预期:<” + x.toString() +

                "> 但发现:<” + y.toString() + ">”);

} else { // 好的,继续

    // ...

}

if  (x.equals(y))  {

    throw  new  AssertionFailedError(

                "expected:  <"  +  x.toString()  +

                ">  but  found:  <"  +  y.toString()  +  ">");

}  else    {  //  Okay,  continue

    //  ...

}

 

不幸的是,此代码将导致NullPointerExceptionif xis null,并且很难将此异常与 SUT 中的错误区分开来。因此,我们需要围绕此功能设置一些保护条款,以便我们始终抛出AssertionFailedException

Unfortunately, this code will cause a NullPointerException if x is null, and it would be hard to distinguish this exception from an error in the SUT. Thus we need to put some guard clauses around this functionality so that we always throw an AssertionFailedException:

if (x == null) { // 不能执行 null.equals(null)

      if (y == null ) { // 它们都为 null 所以相等

          return;

      } else {

            throw new AssertionFailedError(

                "expected null but found: <" + y.toString() +">");

      }

} else if (!x.equals(y)) { // 可比较但不相等!

      throw new AssertionFailedError(

                        "expected: <" + x.toString() +

                        "> but found: <" + y.toString() + ">");

} // 相等

if  (x  ==  null)  {  //  cannot  do  null.equals(null)

      if  (y  ==  null  )  {    //  they  are  both  null  so  equal

          return;

      }  else  {

            throw  new  AssertionFailedError(

                "expected  null  but  found:  <"  +  y.toString()  +">");

      }

}  else  if  (!x.equals(y))  {  //  comparable  but  not  equal!

      throw  new  AssertionFailedError(

                        "expected:  <"  +  x.toString()  +

                        ">  but  found:  <"  +  y.toString()  +  ">");

}  //  equal

 

哎呀!这太乱了。而且我们要对每个要验证的属性都做同样的事情?这可不好。一定有更好的方法。

Yikes! That got pretty messy. And we'll have to do the same thing for every attribute we want to verify? This is not good. There must be a better way.

重构说明

Refactoring Notes

幸运的是,xUnit 的发明者意识到了这个问题,并进行了必要的 Extract Method [Fowler] 重构,创建了一个我们可以调用的断言方法库。我们只需用对适当断言方法的if调用替换乱七八糟的内联语句和抛出的异常。下一个示例是 JUnit 方法的代码。虽然此示例的目的与我们之前编写的代码相同,但它已根据用于标识何时相等的保护条款进行了重写。assertEquals

Luckily for us, the inventors of xUnit recognized this problem and did the requisite Extract Method [Fowler] refactoring to create a library of Assertion Methods that we can call instead. We simply replace the mess of in-line if statements and thrown exceptions with a call to the appropriate Assertion Method. The next example is the code for the JUnit assertEquals method. Although the intent of this example is the same as the code we wrote earlier, it has been rewritten in terms of guard clauses that identify when things are equal.

/**

  * 断言两个对象相等。如果它们不相等,

  * 将抛出 AssertionFailedError 并给出指定消息。

  */

static public void assertEquals(String message,

                                             Object expected,

                                               Object actual) {

      if (expected == null && actual == null)

            return;

      if (expected != null && expected.equals(actual))

            return;

      failNotEquals(message, expected, actual);

}

/**

  *  Asserts  that  two  objects  are  equal.  If  they  are  not,

  *  an  AssertionFailedError  is  thrown  with  the  given  message.

  */

static  public  void  assertEquals(String  message,

                                             Object  expected,

                                               Object  actual)  {

      if  (expected  ==  null  &&  actual  ==  null)

            return;

      if  (expected  !=  null  &&  expected.equals(actual))

            return;

      failNotEquals(message,  expected,  actual);

}

 

该方法failNotEquals是一种测试实用程序方法,它测试失败并提供诊断断言消息。

The method failNotEquals is a Test Utility Method that fails the test and provides a diagnostic assertion message.

例子:平等断言

Example: Equality Assertion

以下是利用 JUnit平等断言而重新编码的相同断言逻辑:

Here is the same assertion logic recoded to take advantage of JUnit's Equality Assertion:

断言等于(x,y);

assertEquals(  x,  y  );

 

以下是用 C# 编写的相同断言。请注意类名限定符以及方法名称中产生的差异:

Here is the same assertion coded in C#. Note the classname qualifier and the resulting difference in the method name:

断言.AreEqual( x, y );

Assert.AreEqual(  x,  y  );

 

示例:模糊相等断言

Example: Fuzzy Equality Assertion

为了比较两个浮点数(它们很少真正相等),我们使用模糊相等断言来指定可接受的差异:

To compare two floating-point numbers (which are rarely ever really equal), we specify the acceptable differences using a Fuzzy Equality Assertion:

断言等于(3.1415,直径/2/半径,0.001);

断言等于(expectedXml,actualXml,elementsToCompare);

assertEquals(  3.1415,  diameter/2/radius,  0.001);

assertEquals(  expectedXml,  actualXml,  elementsToCompare  );

 

示例:陈述结果断言

Example: Stated Outcome Assertion

为了坚持某个特定的结果已经发生,我们使用陈述结果断言:

To insist that a particular outcome has occurred, we use a Stated Outcome Assertion:

断言非空( a );

断言真( b > c );

断言非零( b );

assertNotNull(  a  );

assertTrue(  b  >  c  );

assertNonZero(  b  );

 

示例:预期异常断言

Example: Expected Exception Assertion

下面是一个示例,说明我们如何验证在有块时是否引发了正确的异常。在 Smalltalk 的 SUnit 中,它看起来像这样:

Here is an example of how we verify that the correct exception was raised when we have blocks. In Smalltalk's SUnit, it looks like this:

自我

      应该:[飞行新里程:-1122]

      引发:RuntimeError new'应该引发错误'

self

      should:  [Flight  new  mileage:  -1122]

      raise:  RuntimeError  new  'Should  have  raised  error'

 

表示should:要运行的代码块(用方括号括起来),而raise:指定预期的异常对象。在 Ruby 中,它看起来像这样:

The should: indicates the block of code to run (surrounded by square brackets), while the raise: specifies the expected exception object. In Ruby, it looks like this:

assert_raises( RuntimeError,

                     “应该引发错误”)

                     {flight.setMileage(-1122) }

assert_raises(  RuntimeError,

                     "Should  have  raised  error")

                     {flight.setMileage(-1122)  }

 

do/endRuby 语言语法还允许我们使用这种“控制结构”风格的语法,通过使用而不是花括号来分隔块:

The Ruby language syntax also lets us use this "control structure"-style syntax by delimiting the block using do/end instead of curly braces:

assert_raises( RuntimeError, “应该引发错误”)do

      flight.setMileage(-1122)

end

assert_raises(  RuntimeError,  "Should  have  raised  error")  do

      flight.setMileage(-1122)

end

 

示例:单一结果断言

Example: Single-Outcome Assertion

要使测试失败,请使用单一结果断言:

To fail the test, use the Single Outcome Assertion:

fail( "预期发生异常" );

unfinishedTest();

fail(  "Expected  an  exception"  );

unfinishedTest();

 

断言消息

Assertion Message

我们如何构建测试逻辑来知道哪个断言失败了?

How do we structure our test logic to know which assertion failed?

我们在每次调用断言方法时都包含一个描述性字符串参数。

We include a descriptive string argument in each call to an Assertion Method.

图像

我们通过调用断言方法(第 362页) 来指定预期结果,从而使测试具有自检功能(见第 26页) 。当测试失败时,测试运行器(第 377页) 会将条目写入测试结果日志。

We make tests Self-Checking (see page 26) by including calls to Assertion Methods (page 362) that specify the expected outcome. When a test fails, the Test Runner (page 377) writes an entry to the test result log.

精心设计的断言消息可以很容易地确定哪个断言失败了,以及失败发生时的症状究竟是什么。

A well-crafted Assertion Message makes it very easy to determine which assertion failed and exactly what the symptoms were when the failure happened.

工作原理

How It Works

每个断言方法都带有一个可选的字符串参数,该参数包含在失败日志中。当断言的条件不成立时,断言消息将与断言方法通常生成的输出一起输出到测试运行器的日志中。

Every Assertion Method takes an optional string parameter that is included in the failure log. When the condition being asserted is not true, the Assertion Message is output to the Test Runner's log along with whatever output the assertion method normally generates.

何时使用它

When to Use It

关于这个问题有两种观点。属于“每个测试方法一个断言”派的测试驱动者认为他们不需要包含断言消息,因为只有一个断言可能失败,因此他们总是确切地知道发生了哪个断言。他们依靠断言方法包含参数(例如,预期为“x”,但却是“y”),但他们不需要包含消息。

There are two schools of thought on this subject. Test drivers who belong to the "single assertion per Test Method" school believe that they don't need to include Assertion Messages because only one assertion can possibly fail and, therefore, they always know exactly which assertion happened. They count on the Assertion Method to include the arguments (e.g., expected "x" but was "y") but they don't need to include a message.

相反,如果测试人员在编写多个或多个断言方法调用时遇到问题,则应强烈考虑添加一条消息,至少可以区分哪个断言失败了。如果测试经常使用命令行测试运行器(请参阅测试运行器)运行,则此信息尤其重要,因为命令行测试运行器很少提供失败位置信息。

Conversely, people who find themselves coding several or many assertion method calls in their tests should strongly consider including a message that at least distinguishes which assertion failed. This information is especially important if the tests are frequently run using a Command-Line Test Runner (see Test Runner), which rarely provides failure location information.

实施说明

Implementation Notes

很容易说我们需要为每个断言方法调用添加一条消息 — 但我们应该在消息中说些什么呢?在编写每个断言时花点时间问问自己,阅读失败日志的人希望从中得到什么,这很有用。

It is easy to state that we need a message for each assertion method call—but what should we say in the message? It is useful to take a moment as we write each assertion and ask ourselves what the person reading the failure log would hope to get out of it.

变体:断言识别消息

当我们在同一个测试方法(第 348页)中包含多个同类型的断言时,我们会更难确定到底是哪一个断言失败了。通过在每个断言消息中包含一些唯一的文本,我们可以很容易地确定哪个断言方法调用失败了。一种常见的做法是使用被断言的变量或属性的名称作为消息。这种技术非常简单,几乎不需要思考。另一种选择是对断言进行编号。这些信息当然是唯一的,但理解它可能不那么直观,因为我们必须查看代码来确定哪个断言失败了。

When we include several assertions of the same type in the same Test Method (page 348), we make it more difficult to determine exactly which one failed the test. By including some unique text in each Assertion Message, we can make it very easy to determine which assertion method call failed. A common practice is to use the name of the variable or attribute being asserted on as the message. This technique is very simple and requires very little thought. Another option is to number the assertions. This information would certainly be unique but understanding it may be less intuitive as we would have to look at the code to determine which assertion was failing.

变体:描述期望的信息

当测试失败时,我们知道实际发生了什么。最大的问题是“应该发生什么?”有几种方法可以为测试阅读者记录预期行为。例如,我们可以在测试代码中添加注释。更好的解决方案是在断言消息中包含对期望的描述。虽然对于相等性断言(参见断言方法),这是自动完成的,但对于任何陈述结果断言(参见断言方法) ,我们需要自己提供此信息。

When a test fails, we know what has actually happened. The big question is, "What should have happened?" There are several ways of documenting the expected behavior for the test reader. For example, we could place comments in the test code. A better solution is to include a description of the expectation in the Assertion Message. While this is done automatically for an Equality Assertion (see Assertion Method), we need to provide this information ourselves for any Stated Outcome Assertions (see Assertion Method).

变体:参数描述消息

某些类型的断言方法提供的失败消息比其他类型的断言方法提供的失败消息帮助更小。最糟糕的是陈述结果断言,例如assertTrue(aBooleanExpression)。当它们失败时,我们只知道陈述的结果没有发生。在这些情况下,我们可以将正在评估的表达式(包括实际值)作为断言消息文本的一部分。然后,测试维护者可以检查失败日志并确定正在评估的内容以及导致测试失败的原因。

Some types of Assertion Methods provide less helpful failure messages than others. Among the worst are Stated Outcome Assertions such as assertTrue(aBooleanExpression). When they fail, all we know is that the stated outcome did not occur. In these cases we can include the expression that was being evaluated (including the actual values) as part of the Assertion Message text. The test maintainer can then examine the failure log and determine what was being evaluated and why it caused the test to fail.

激励人心的例子

Motivating Example

断言真(a > b);

断言真(b > c);

assertTrue(  a  >  b    );

assertTrue(  b  >  c  );

 

此代码发出一条失败消息,类似于“断言失败”。从此输出,我们甚至无法判断两个断言消息中的哪一个失败了。没什么用,不是吗?

This code emits a failure message—something like "Assertion Failed." From this output, we cannot even tell which of the two Assertion Messages failed. Not very useful, is it?

重构说明

Refactoring Notes

解决这个问题很简单,只需在每个断言方法调用中添加一个参数即可。在本例中,我们想要传达的是,我们期望“a”大于“b”。当然,能够看到“a”和“b”的实际值也很有用。我们可以通过一些明智的字符串连接将这两条信息添加到断言消息中。

Fixing this problem is a simple matter of adding one more parameter to each Assertion Method call. In this case, we want to communicate that we are expecting "a" to be greater than "b". Of course, it would also be useful to be able to see what the values of "a" and "b" actually were. We can add both pieces of information into the Assertion Message through some judicious string concatenation.

示例:描述期望的消息

Example: Expectation-Describing Message

以下是添加了参数描述消息的相同测试:

Here is the same test with the Argument-Describing Message added:

assertTrue( "预期 a > b 但 a 为 '" + a.toString() +

                              "' 且 b 为 '" + b.toString() + "'",

                  a.gt(b) );

assertTrue( "预期 b > c 但 b 为 '" + b.toString() +

                             "' 且 c 为 '" + c.toString + "'",

                 b > c );

assertTrue(  "Expected  a  >  b  but  a  was  '"  +  a.toString()  +

                              "'  and  b  was  '"  +  b.toString()  +  "'",

                  a.gt(b)  );

assertTrue(  "Expected  b  >  c  but  b  was  '"  +  b.toString()  +

                             "'  and  c  was  '"  +  c.toString  +  "'",

                 b  >  c  );

 

这将导致一个有用的失败消息:

This will now result in a useful failure message:

断言失败。预期 a > b,但 a 为“17”,b 为“19”。

Assertion  Failed.  Expected  a  >  b  but  a  was  '17'  and  b  was  '19'.

 

当然,如果变量具有意图揭示名称[SBPP],这个输出将更有意义!

Of course, this output would be even more meaningful if the variables had Intent-Revealing Names [SBPP]!

测试用例类

Testcase Class

也称为

Also known as

测试治具

Test Fixture

我们的测试代码应该放在哪里?

Where do we put our test code?

我们将一组相关的测试方法分组到单个测试用例类中。

We group a set of related Test Methods on a single Testcase Class.

图像

我们将测试逻辑放入测试方法(第 348页),但这些测试方法需要与一个类相关联。测试用例类为我们提供了一个地方来托管这些方法,我们稍后可以将其转换为测试用例对象(第 382页)。

We put our test logic into Test Methods (page 348) but those Test Methods need to be associated with a class. A Testcase Class gives us a place to host those methods that we can later turn into Testcase Objects (page 382).

工作原理

How It Works

我们将所有以某种方式相关的测试方法收集到一种特殊的类中,即测试用例类。在运行时,测试用例类充当测试套件工厂(请参阅第 399页的测试枚举),为每个测试方法创建一个测试用例对象它将所有这些对象添加到测试套件对象第 387页),测试运行器(第377页)将使用该对象运行所有对象。

We collect all Test Methods that are related in some way onto a special kind of class, the Testcase Class. At runtime, the Testcase Class acts as a Test Suite Factory (see Test Enumeration on page 399) that creates a Testcase Object for each Test Method. It adds all of these objects to a Test Suite Object (page 387) that the Test Runner (page 377) will use to run them all.

我们为什么这样做

Why We Do This

在面向对象语言中,我们倾向于将测试方法放在类中,而不是将它们作为全局函数或过程(即使允许这样做)。通过将它们作为测试用例类的实例方法,我们可以为每个测试方法实例化一次测试用例类,从而为每个测试创建一个测试用例对象。此策略允许我们在运行时操纵测试方法

In object-oriented languages, we prefer to put our Test Methods onto a class rather than having them as global functions or procedures (even if that is allowed). By making them instance methods of a Testcase Class, we can create a Testcase Object for each test by instantiating the Testcase Class once for each Test Method. This strategy allows us to manipulate the Test Methods at runtime.


类-实例二元性

回到高中物理课,我们学习了光的“波粒二象性”。有时光表现得像粒子(例如,穿过一个小孔),有时又像波(例如,彩虹)。测试用例类第 373页)的行为有时会让我想起这个概念。让我来解释一下原因。

刚接触 xUnit 的开发人员经常会问,“既然我们TestCase有多个测试方法,为什么我们要将其子类化?难道它不应该被称为TestSuite?”当我们在编写测试代码时(而不是在运行代码时)主要关注类的视图时,这些问题就很有意义了。

编写测试代码时,我们专注于测试方法测试用例类主要只是放置方法的地方。我们唯一会想到对象的时候是使用隐式设置(第424页)时,需要创建字段(实例变量)以在方法调用和测试方法setUp中使用它们之间保存它们。xUnit 测试自动化新手开发人员在编写第一个测试时,他们倾向于通过示例进行编码。遵循现有示例是让某些东西快速运行的好方法,但它并不一定能帮助开发人员了解到底发生了什么。

在运行时,xUnit 框架通常为每个测试方法创建一个测试用例类实例。测试用例类充当测试套件工厂(请参阅第 399页的测试枚举),它构建一个包含其自身所有实例的测试套件对象(第387页),每个测试方法一个实例。现在,类上的静态方法很少返回包含其自身许多实例的另一个类的实例。如果这种行为还不够奇怪,xUnit 使用测试方法名称报告测试失败这一事实足以让许多测试自动化人员无法察觉“内部对象”的存在。

当我们在运行时检查对象关系时,事情会变得更加清晰。测试套件工厂返回的测试套件对象包含一个或多个测试用例对象第 382页)。到目前为止,一切顺利。每个对象都是我们的测试用例类的一个实例。每个实例都配置为运行其中一种测试方法。更重要的是,每个实例将运行不同的测试方法。(有关如何发生这种情况的更详细描述,请参见第 393页的测试发现。)因此,我们的测试用例类的每个实例实际上都是一个测试用例。测试方法只是我们告诉每个实例它应该测试什么的方式。

进一步阅读

Martin Fowler 在他的博客上有一篇关于此问题的精彩文章,名为“JUnit New Instance” [JNI]



Class–Instance Duality

Back in high school physics, we learned about the "wave–particle duality" of light. Sometimes light acts like a particle (e.g., going through a small aperture), and sometimes it acts like a wave (e.g., rainbows). The behavior of Testcase Classes (page 373) sometimes reminds me of this concept. Let me explain why.

Developers new to xUnit often ask, "Why is the class we subclass called TestCase when we have several Test Methods on it? Shouldn't it be called TestSuite?" These questions make a lot of sense when we are focused primarily on the view of the class when we are writing the test code as opposed to when we are running the code.

When we are writing test code, we concentrate on the Test Methods. The Testcase Class is primarily just a place to put the methods. About the only time we think of objects is when we use Implicit Setup (page 424) and need to create fields (instance variables) to hold them between the invocation of the setUp method and when they are used in the Test Method. When developers new to xUnit test automation are writing their first tests, they tend to code by example. Following an existing example is a good way to get something working quickly but it doesn't necessarily help the developer understand what is really going on.

At runtime, the xUnit framework typically creates one instance of the Testcase Class for each Test Method. The Testcase Class acts as a Test Suite Factory (see Test Enumeration on page 399) that builds a Test Suite Object (page 387) containing all the instances of itself, one instance for each Test Method. Now, it's not very often that a static method on a class returns an instance of another class containing many instances of itself. If this behavior wasn't odd enough, the fact that xUnit reports the test failures using the Test Method name can be enough to obscure from many test automaters the existence of "objects inside."

When we examine the object relationships at runtime, things become a bit clearer. The Test Suite Object returned by the Test Suite Factory contains one or more Testcase Objects (page 382). So far, so good. Each of these objects is an instance of our Testcase Class. Each instance is configured to run one of the Test Methods. More importantly, each will run a different Test Method. (How this happens is described in more detail in Test Discovery on page 393.) So each instance of our Testcase Class is, indeed, a test case. The Test Methods are just how we tell each instance what it should test.

Further Reading

Martin Fowler has a great piece on his blog about this issue called "JUnit New Instance" [JNI].


 

当然,我们可以在单独的类中实现每个测试方法— — 但这会产生额外的开销并使类命名空间变得混乱。这也使得在测试之间重用功能变得更加困难(尽管并非不可能)。

We could, of course, implement each Test Method on a separate class—but that creates additional overhead and clutters the class namespace. It also makes it harder (although not impossible) to reuse functionality between tests.

实施说明

Implementation Notes

编写测试的大部分复杂性涉及如何编写测试方法:哪些内容需要以内联方式包含,哪些内容需要分解到测试实用程序方法(第 599页) 中,如何隔离 SUT (参见第43页),等等。

Most of the complexity of writing tests involves how to write the Test Methods: what to include in-line and what to factor out into Test Utility Methods (page 599), how to Isolate the SUT (see page 43), and so on.

Testcase 类的真正魔力发生在运行时,如Testcase 对象测试运行器中所述。就我们而言,我们所要做的就是编写一些包含测试逻辑的测试方法,然后让测试运行器发挥它的魔力。我们可以通过使用提取方法 [Fowler] 重构将通用代码分解到测试实用方法中来避免测试代码重复第 213页) 。这些方法可以留在Testcase 类中,也可以移至抽象 Testcase超类(请参阅第 638页的测试用例超类)、测试助手类(第 643页)或测试助手 Mixin(请参阅测试用例超类)。

The real magic associated with the Testcase Class occurs at runtime and is described in Testcase Object and Test Runner. As far as we are concerned, all we have to do is write some Test Methods that contain our test logic and let the Test Runner work its magic. We can avoid Test Code Duplication (page 213) by using an Extract Method [Fowler] refactoring to factor out common code into Test Utility Methods. These methods can be left on the Testcase Class or they can be moved to an Abstract Testcase superclass (see Testcase Superclass on page 638), a Test Helper class (page 643), or a Test Helper Mixin (see Testcase Superclass).

示例:测试用例类

Example: Testcase Class

这是一个简单测试用例类的示例:

Here is an example of a simple Testcase Class:

public class TestScheduleFlight 扩展了 TestCase {



      public void testUnscheduled_shouldEndUpInScheduled() 抛出异常 {

            Flight flight = FlightTestHelper。

                    getAnonymousFlightInUnscheduledState();

          flight.schedule();

          assertTrue( "isScheduled()", flight.isScheduled());

      }



      public void testScheduledState_shouldThrowInvalidRequestEx()

                    抛出异常 {

            Flight flight = FlightTestHelper。

                        getAnonymousFlightInScheduledState();

            尝试 {

                  flight.schedule();

                  fail("不允许处于预定状态");

            } catch (InvalidRequestException e) {

                  assertEquals("InvalidRequestException.getRequest()",

                                     "schedule",

                                    e.getRequest());

                  assertTrue( "isScheduled()", flight.isScheduled());

            }

      }



      public void testAwaitingApproval_shouldThrowInvalidRequestEx()

                    throws Exception {

            Flight flight = FlightTestHelper.

                    getAnonymousFlightInAwaitingApprovalState();

            try {

                  flight.schedule();

                  fail("不允许处于计划状态");

            } catch (InvalidRequestException e) {

                  assertEquals("InvalidRequestException.getRequest()",

                                    "schedule",

                                    e.getRequest());

                  assertTrue( "isAwaitingApproval()",

                                    flight.isAwaitingApproval());

            }

      }

}

public  class  TestScheduleFlight  extends  TestCase  {



      public  void  testUnscheduled_shouldEndUpInScheduled()  throws  Exception  {

            Flight  flight  =  FlightTestHelper.

                    getAnonymousFlightInUnscheduledState();

          flight.schedule();

          assertTrue(  "isScheduled()",  flight.isScheduled());

      }



      public  void  testScheduledState_shouldThrowInvalidRequestEx()

                    throws  Exception  {

            Flight  flight  =  FlightTestHelper.

                        getAnonymousFlightInScheduledState();

            try  {

                  flight.schedule();

                  fail("not  allowed  in  scheduled  state");

            }  catch  (InvalidRequestException  e)  {

                  assertEquals("InvalidRequestException.getRequest()",

                                     "schedule",

                                    e.getRequest());

                  assertTrue(    "isScheduled()",  flight.isScheduled());

            }

      }



      public  void  testAwaitingApproval_shouldThrowInvalidRequestEx()

                    throws  Exception  {

            Flight  flight  =  FlightTestHelper.

                    getAnonymousFlightInAwaitingApprovalState();

            try  {

                  flight.schedule();

                  fail("not  allowed  in  schedule  state");

            }  catch  (InvalidRequestException  e)  {

                  assertEquals("InvalidRequestException.getRequest()",

                                    "schedule",

                                    e.getRequest());

                  assertTrue(    "isAwaitingApproval()",

                                    flight.isAwaitingApproval());

            }

      }

}

 
进一步阅读

在 xUnit 的某些变体(最显著的是 VbUnit 和 NUnit)中,Testcase 类被称为测试装置。此用法不应与测试装置(或测试上下文)混淆,后者包含我们在开始执行 SUT 之前需要准备的一切。3也不应将其与 Fit 框架使用的装置术语混淆,后者是与 Fit 表交互的适配器[GOF],从而实现数据驱动测试第 288页)解释器[GOF]

In some variants of xUnit (most notably VbUnit and NUnit), the Testcase Class is called a test fixture. This usage should not be confused with the test fixture (or test context) that consists of everything we need to have in place before we can start exercising the SUT.3 Neither should it be confused with the fixture term as used by the Fit framework, which is the Adapter [GOF] that interacts with the Fit table and thereby implements a Data-Driven Test (page 288) Interpreter [GOF].

测试运行器

Test Runner

我们如何运行测试?

How do we run the tests?

我们定义一个应用程序,实例化一个测试套件对象并执行它包含的所有测试用例对象。

We define an application that instantiates a Test Suite Object and executes all the Testcase Objects it contains.

图像

假设我们已经在一个或多个测试用例类(第 373页)上定义了测试方法第 348页),那么我们如何真正地让测试自动化框架(第298页)运行我们的测试呢?

Assuming we have defined our Test Methods (page 348) on one or more Testcase Classes (page 373), how do we actually cause the Test Automation Frameworks (page 298) to run our tests?

工作原理

How It Works

测试自动化框架xUnit 系列中的每个成员都提供了某种形式的命令行或图形应用程序,可用于运行自动化测试并报告结果。测试运行器使用测试枚举第 399页)、测试发现第 393页)或测试选择(第403页)来获取复合[GOF]测试对象。后者可以是单个测试用例对象第 382页)、测试套件对象第 387页)或复合测试套件(套件套件请参阅测试套件对象)。因为所有这些对象都实现相同的接口,所以测试运行器不需要关心它是处理的是单个测试还是多级套件。测试运行器会跟踪并报告它运行了多少个测试、有多少个测试断言失败以及有多少个测试引发了错误或异常。

Each member of the xUnit family of Test Automation Frameworks provides some form of command-line or graphical application that can be used to run our automated tests and report on the results. The Test Runner uses Test Enumeration (page 399), Test Discovery (page 393), or Test Selection (page 403) to obtain a Composite [GOF] test object. The latter may either be a single Testcase Object (page 382), a Test Suite Object (page 387), or a Composite test suite (a Suite of Suites; see Test Suite Object). Because all of these objects implement the same interface, the Test Runner need not care whether it is dealing with a single test or a multilevel suite. The Test Runner keeps track of, and reports on, how many tests it has run, how many tests had failed assertions, and how many tests raised errors or exceptions.

我们为什么这样做

Why We Do This

我们不希望每个测试自动化人员都必须提供一种特殊的方式来运行自己的测试套件。这种要求只会妨碍我们同时运行由不同人员自动化的所有测试的能力。通过提供标准的测试运行器,我们鼓励开发人员轻松运行由不同人员编写的测试。我们还可以提供运行相同测试的不同方法。

We wouldn't want each test automater to have to provide a special means of running his or her own test suites. That requirement would just get in the way of our ability to simultaneously run all the tests automated by different people. By providing a standard Test Runner, we encourage developers to make it easy to run tests written by different people. We can also provide different ways of running the same tests.

实施说明

Implementation Notes

有多种类型的测试运行器可供选择。最常见的变体是从 IDE 中运行测试和从命令行运行测试。所有这些方案都依赖于所有测试用例对象都实现标准接口这一事实。

Several styles of Test Runners are available. The most common variations are running tests from within an IDE and running tests from the command line. All of these schemes depend on the fact that all Testcase Objects implement a standard interface.

标准测试接口

静态类型语言(如 Java 和 C#)通常包含一个接口类型(完全抽象类),该接口定义所有测试用例对象测试套件对象必须实现的接口。某些语言(如 C# 和 Java 5.0)通过在测试用例类上使用类属性或注释来“混合”实现。在动态类型语言中,此接口可能不明确存在。相反,每个实现类仅实现标准接口方法。通常,标准测试接口包含用于计算可用测试和运行测试的方法。如果框架支持测试枚举,则每个测试用例类和测试套件类还必须实现测试套件工厂方法(请参阅第399页的测试枚举),该方法通常称为。suite

Statically typed languages (such as Java and C#) typically include an interface type (fully abstract class) that defines the interface that all Testcase Objects and Test Suite Objects must implement. Some languages (such as C# and Java 5.0) "mix" in the implementation by using class attributes or annotations on the Testcase Class. In dynamically typed languages, this interface may not exist explicitly. Instead, each implementation class simply implements the standard interface methods. Typically, the standard test interface includes methods on it to count the available tests and to run the tests. Where the framework supports Test Enumeration, each Testcase Class and test suite class must also implement the Test Suite Factory method (see Test Enumeration on page 399), which is typically called suite.

变体:图形测试运行器

图形测试运行器通常是用于运行测试的桌面应用程序或 IDE(内置或插件)的一部分。(见图19.1。)至少有一个,IeUnit,在 Web 浏览器而不是 IDE 中运行。图形测试运行器最常见的功能是某种实时进度指示器。此监视器通常包括测试失败和错误的运行计数,并且通常包括一个彩色进度条,该进度条从绿色开始,一旦遇到错误或失败就会变为红色。xUnit 家族的一些成员包含一个图形测试树资源管理器,作为在套件套件中深入研究和运行单个测试的一种方式。

A Graphical Test Runner is typically a desktop application or part of an IDE (either built-in or a plug-in) for running tests. (See Figure 19.1.) At least one, IeUnit, runs inside a Web browser rather than an IDE. The most common feature of the Graphical Test Runner is some sort of real-time progress indicator. This monitor typically includes a running count of test failures and errors and often includes a colored progress bar that starts off green and turns red as soon as an error or failure is encountered. Some members of the xUnit family include a graphical Test Tree Explorer as a means to drill down and run a single test from within a Suite of Suites.

以下是Eclipse 的 JUnit 插件中的图形测试运行器:

Here is the Graphical Test Runner from the JUnit plug-in for Eclipse:

图 19.1。Eclipse Java IDE 的 JUnit 插件的图形测试运行器。

Figure 19.1. Graphical Test Runner from the JUnit plug-in for Eclipse Java IDE.

图像

顶部附近的红色条表示至少有一项测试失败。上方的文本窗格显示测试失败和测试错误的列表。下方窗格显示上方窗格中所选失败测试的回溯。

The red bar near the top indicates that at least one test has failed. The upper text pane shows a list of test failures and test errors. The lower pane shows the traceback from the failed test selected in the upper pane.

变体:命令行测试运行器

命令行测试运行器旨在通过操作系统命令行、批处理文件或 shell 脚本使用。当通过远程 shell 进行远程工作或从构建脚本(例如“make”、 Ant)持续集成工具(例如“Cruise Control”)运行测试时,它们非常方便。

Command-Line Test Runners are designed to be used from an operating system command line or from batch files or shell scripts. They are very handy when working remotely via remote shells or when running the tests from a build script such as "make," Ant, or a continuous integration tool such as "Cruise Control."

以下示例显示如何从命令行运行runit(Ruby 编程语言的 xUnit 实现之一)测试:

The following example shows how to run an runit (one of the xUnit implementations for the Ruby programming language) test from the command line:

>ruby testrunner.rb c:/examples/tests/SmellHandlerTest.rb

已加载套件 SmellHandlerTest

已开始

.....

0.016 秒内完成。5

个测试,6 个断言,0 个失败,0 个错误

>退出代码:0

>ruby  testrunner.rb  c:/examples/tests/SmellHandlerTest.rb

Loaded  suite  SmellHandlerTest

Started

.....

Finished  in  0.016  seconds.

5  tests,  6  assertions,  0  failures,  0  errors

>Exit  code:  0

 

第一行是在命令提示符下的调用。在此示例中,我们正在运行在单个Testcase 类中定义的测试。SmellHandlerTest接下来的两行是测试开始时的初始反馈。一系列点表示测试的进度,每完成一个测试就有一个点。如果测试产生错误或失败,此特定的命令行测试运行器会将点替换为“E”或“F”。最后三行是汇总统计信息,概述了所发生的情况。通常,退出代码设置为失败/错误测试的总数,因此从自动构建工具运行时,非零退出代码很容易被解释为构建失败。

The first line is the invocation at the command prompt. In this example we are running the tests defined in a single Testcase Class, SmellHandlerTest. The next two lines are the initial feedback as the tests begin. The series of dots indicates the tests' progress, one per test completed. This particular Command-Line Test Runner replaces the dot with an "E" or an "F" if the test produces an error or fails. The last three lines are summary statistics that provide an overview of what happened. Typically, the exit code is set to the total number of failed/error tests so that a non-zero exit code can be interpreted easily as a build failure when run from an automated build tool.

变体:文件系统测试运行器

一些命令行测试运行器提供了在指定目录中搜​​索所有测试文件并一次性运行它们的选项。这种自动化的测试用例类发现(请参阅测试发现)避免了在代码中构建套件套件(测试枚举)的需要,并有助于避免丢失测试(请参阅第268页的生产错误)。

Some Command-Line Test Runners provide the option of searching a specified directory for all files that are tests and running them all at once. This automated Testcase Class Discovery (see Test Discovery) avoids the need to build the Suite of Suites in code (Test Enumeration) and helps avoid Lost Tests (see Production Bugs on page 268).

此外,一些外部工具会在文件系统中搜索与特定模式匹配的文件,然后针对匹配的文件调用任意命令。这些文件可以从构建工具传递给测试运行器。

In addition, some external tools will search the file system for files matching specific patterns and then invoke an arbitrary command against the matched files. These files can be passed to the Test Runner from a build tool.

变体:测试树资源管理器

xUnit 系列的成员将每个测试方法转换为测试用例对象,可以轻松操作测试。其中许多都提供了套件套件的图形表示,并允许用户选择要运行的整个测试套件对象或单个测试用例对象。这样就无需创建单个测试套件(请参阅第 592页的命名测试套件)类来运行单个测试。

Members of the xUnit family that turn each Test Method into a Testcase Object can manipulate the tests easily. Many of them provide a graphical representation of the Suite of Suites and allow the user to select an entire Test Suite Object or a single Testcase Object to run. This eliminates the need to create a Single Test Suite (see Named Test Suite on page 592) class to run a single test.

图 19.2显示了Eclipse 的 JUnit 插件的测试树资源管理器在其他 Eclipse 视图上“弹出”的情况:

Figure 19.2 shows the Test Tree Explorer of JUnit plug-in for Eclipse shown "popped out" over other Eclipse views:

图 19.2. Eclipse Java IDE 内部显示的JUnit测试树资源管理器。

Figure 19.2. JUnit's Test Tree Explorer shown inside Eclipse Java IDE.

图像

IDE 的左侧窗格是 Eclipse 中的 JUnit 视图。进度条显示在视图的顶部,上部窗格是测试树资源管理器,下部窗格是当前选定测试失败的回溯。请注意,测试树资源管理器中的某些测试套件对象处于“打开”状态,显示其内容;其他测试套件对象处于关闭状态。每个测试用例对象旁边的彩色注释显示其状态;每个测试套件对象的注释指示任何包含的测试用例对象是否失败或产生错误。名为“ ”的测试套件对象是包“ ”的套件套件;“ ”是运行所有测试的最顶层套件套件。Test  for  com.clrstream.ex8.testcom.clrstream.ex8.testTest  for  allJUnitTests

The left pane of the IDE is the JUnit view within Eclipse. The progress bar appears at the top of the view, the upper pane is the Test Tree Explorer, and the lower pane is the traceback for the currently selected test failure. Note that some Test Suite Objects in the Test Tree Explorer are "open," revealing their contents; others are closed down. The colored annotation next to each Testcase Object shows its status; the annotations for each Test Suite Object indicate whether any contained Testcase Objects failed or produced an error. The Test Suite Object called "Test  for  com.clrstream.ex8.test" is a Suite of Suites for the package "com.clrstream.ex8.test"; "Test  for  allJUnitTests" is the topmost Suite of Suites for running all the tests.

测试用例对象

Testcase Object

我们如何运行测试?

How do we run the tests?

我们为每个测试创建一个 Command 对象,并run在希望执行时调用该方法。

We create a Command object for each test and call the run method when we wish to execute it.

图像

测试运行器(第377页) 需要一种方法来查找和调用适当的测试方法(第 348页) 并向用户呈现结果。许多图形测试运行器(参见测试运行器) 允许用户深入到测试树中并挑选要运行的单个测试。此功能要求测试运行器能够在运行时检查和操作测试。

The Test Runner (page 377) needs a way to find and invoke the appropriate Test Methods (page 348) and to present the results to the user. Many Graphical Test Runners (see Test Runner) let the user drill down into the tree of tests and pick individual tests to run. This capability requires that the Test Runner be able to inspect and manipulate the tests at runtime.

工作原理

How It Works

我们实例化一个命令[GOF]对象来表示应执行的每个测试方法。我们使用测试用例类(第 373页) 作为测试套件工厂来创建测试套件对象(第 387页) 以保存特定测试用例类的所有测试用例对象。我们可以使用测试发现(第 393页) 或测试枚举来创建测试用例对象

We instantiate a Command [GOF] object to represent each Test Method that should execute. We use the Testcase Class (page 373) as a Test Suite Factory to create a Test Suite Object (page 387) to hold all the Testcase Objects for a particular Testcase Class. We can use either Test Discovery (page 393) or Test Enumeration to create the Testcase Objects.

我们为什么这样做

Why We Do This

将测试视为一等对象会开启许多新的可能性,而如果我们将测试视为简单的程序,这些可能性是无法实现的。当测试是对象时,测试自动化框架(第 298页)的测试运行器可以更轻松地操纵测试。我们可以将它们保存在集合 (测试套件对象) 中,对它们进行迭代、调用它们等等。

Treating tests as first-class objects opens up many new possibilities that are not available to us if we treat the tests as simple procedures. It is a lot easier for the Test Runner of the Test Automation Framework (page 298) to manipulate tests when they are objects. We can hold them in collections (Test Suite Objects), iterate over them, invoke them, and so on.

xUnit 系列的大多数成员都为每个测试创建单独的测试用例对象,以按照独立测试(参见第 42页)的规定将测试彼此隔离。不幸的是,总会有例外 (参见第384页的侧栏“总会有例外” ),受影响的测试自动化框架的用户需要更加谨慎。

Most members of the xUnit family create a separate Testcase Object for each test to isolate the tests from one another as prescribed by Independent Test (see page 42). Unfortunately, there is always an exception (see the sidebar "There's Always an Exception" on page 384), and users of the affected Test Automation Frameworks need to be a bit more cautious.

实施说明

Implementation Notes

每个测试用例对象都实现了一个标准测试接口,因此测试运行器无需知道每个测试的具体接口。此方案允许每个测试用例对象充当命令对象[GOF]。这使我们能够构建这些测试用例对象的集合,然后我们可以遍历这些集合来执行计数、运行、显示和其他操作。

Each Testcase Object implements a standard test interface so that the Test Runner does not need to know the specific interface for each test. This scheme allows each Testcase Object to act as a Command object [GOF]. This allows us to build collections of these Testcase Objects, which we can then iterate across to do counting, running, displaying, and other operations.

在大多数编程语言中,我们需要创建一个类来定义测试用例对象的行为。我们可以为每个测试用例对象创建一个单独的测试用例类。但是,将许多测试方法托管在一个测试用例类中更为方便,因为这种策略可以减少需要管理的类,并有助于重用测试实用方法第 599页)。这种方法要求测试用例类的每个测试用例对象都有一种方式来确定应该调用哪个测试方法。可插入行为[SBPP]是执行此操作的最常见方法。测试用例类的构造函数将要调用的方法的名称作为参数,并将此名称存储在实例变量中。当测试运行器调用测试用例对象上的方法时,它使用反射来查找并调用名称在实例变量中的方法。run

In most programming languages, we need to create a class to define the behavior of the Testcase Objects. We could create a separate Testcase Class for each Testcase Object. It is more convenient to host many Test Methods on a single Testcase Class, however, as this strategy results in fewer classes to manage and facilitates reuse of Test Utility Methods (page 599). This approach requires that each Testcase Object of the Testcase Class have a way to determine which Test Method it should invoke. Pluggable Behavior [SBPP] is the most common way to do this. The constructor of the Testcase Class takes the name of the method to be invoked as a parameter and stores this name in an instance variable. When the Test Runner invokes the run method on the Testcase Object, it uses reflection to find and invoke the method whose name is in the instance variable.


总会有例外

无论我们是学习用一门新语言来变位动词,还是寻找软件构建的模式,总会有例外!

xUnit 家族中最值得注意的例外之一与使用测试用例对象(第 382页)在运行时表示每个测试方法 (第 348页) 有关。xUnit 的这一关键设计特性提供了一种实现独立测试(参见第42页) 的方法。xUnit 家族中唯一不遵循此方案的成员是 TestNG 和 NUnit (版本 2.x)。由于下面所述的原因,NUnit 2.0 的构建者选择偏离每个测试方法一个测试用例对象的陈旧方法,并仅创建测试用例类 (第373页) 的单个实例。这个实例被称为测试装置,然后被每个测试方法重用。NUnit 2.0 的作者之一 James Newkirk 写道:

我认为,编写 NUnit V2.0 时犯的最大错误之一是没有为所包含的每种测试方法创建测试装置类的新实例。我说的是“我们”,但我认为这是我的错。我不太理解 JUnit 中为每种测试方法创建测试装置新实例的理由。我现在回过头来看,发现重用每种测试方法的实例允许某人存储一个测试的成员变量并在另一个测试中使用它。这可能会引入执行顺序依赖关系,对于这种类型的测试来说,这是一种反模式。最好将每种测试方法与其他测试方法完全隔离。这要求为每种测试方法创建一个新对象。

 

不幸的是,如果熟悉“JUnit 新实例行为”,即每个方法都有一个单独的测试用例对象,那么这会产生一些非常有趣且不受欢迎的后果。由于该对象被重用,因此它通过实例变量引用的任何对象都可用于所有后续测试。这会导致隐式共享夹具第 317页)以及与之相关的所有形式的异常测试(第228页)。詹姆斯继续说:

由于现在很难改变 NUnit 的工作方式,而且会有太多人抱怨,所以我现在将测试装置类中的所有成员变量都设为静态。这几乎就像广告中的真相一样。结果是,无论创建了多少个测试装置对象,这个变量都只有一个实例。如果变量是静态的,那么可能不熟悉 NUnit 执行方式的人就不会认为在执行每个测试之前都会创建一个新的变量。这是我在不改变 NUnit 执行测试方法的方式的情况下最接近 JUnit 的工作方式。

 

Martin Fowler 认为这个异常非常重要,因此他写了一篇文章来解释为什么 JUnit 的方法是正确的。请参阅http://martinfowler.com/bliki/JunitNewInstance.html



There's Always an Exception

Whether we are learning to conjugate verbs in a new language or looking for patterns in how software is built, there's always an exception!

One of the most notable exceptions in the xUnit family relates to the use of a Testcase Object (page 382) to represent each Test Method (page 348) at runtime. This key design feature of xUnit offers a way to achieve an Independent Test (see page 42). The only members of the xUnit family that don't follow this scheme are TestNG and NUnit (version 2.x). For the reasons described below, the builders of NUnit 2.0 chose to stray from the well-worn path of one Testcase Object per Test Method and create only a single instance of the Testcase Class (page 373). This instance, which they call the test fixture, is then reused for each Test Method. One of the authors of NUnit 2.0, James Newkirk, writes:

I think one of the biggest screw-ups that was made when we wrote NUnit V2.0 was to not create a new instance of the test fixture class for each contained test method. I say "we" but I think this one was my fault. I did not quite understand the reasoning in JUnit for creating a new instance of the test fixture for each test method. I look back now and see that reusing the instance for each test method allows someone to store a member variable from one test and use it in another. This can introduce execution-order dependencies, which for this type of testing is an anti-pattern. It is much better to fully isolate each test method from the others. This requires that a new object be created for each test method.

 

Unfortunately, this has some very interesting—and undesirable—consequences when one is familiar with the "JUnit New Instance Behavior" of a separate Testcase Object per method. Because the object is reused, any objects it refers to via an instance variable are available to all subsequent tests. This results in an implicit Shared Fixture (page 317) along with all the forms of Erratic Tests (page 228) that go with it. James goes on to say:

Since it would be difficult to change the way that NUnit works now, and too many people would complain, I now make all of the member variables in test fixture classes static. It's almost like truth in advertising. The result is that there is only one instance of this variable, no matter how many test fixture objects are created. If the variable is static, then someone who may not be familiar with how NUnit executes would not assume that a new one is created before each test is executed. This is the closest I can get to how JUnit works without changing the way that NUnit executes test methods.

 

Martin Fowler felt this exception was important enough that he wrote an article about why JUnit's approach is correct. See http://martinfowler.com/bliki/JunitNewInstance.html.


 

示例:测试用例对象

Example: Testcase Object

当我们“深入”测试套件对象以显示其包含的测试用例对象时,测试用例对象存在的主要证据出现在测试树资源管理器中(参见测试运行器)。让我们看一个来自 Eclipse 内置的 JUnit图形测试运行器的示例。以下是从测试用例类编写的示例代码创建的对象列表

The main evidence of the existence of Testcase Objects appears in the Test Tree Explorer (see Test Runner) when we "drill down" into the Test Suite Object to expose the Testcase Objects it contains. Let's look at an example from the JUnit Graphical Test Runner that is built into Eclipse. Here's the list of objects created from the sample code from the write-up of Testcase Class:

TestSuite("...flightstate.featuretests.AllTests")

      TestSuite("...flightstate.featuretests.TestApproveFlight")

            TestApproveFlight("testScheduledState_shouldThrowIn..ReEx")

            TestApproveFlight("testUnsheduled_shouldEndUpInAwai..oval")

            TestApproveFlight("testAwaitingApproval_shouldThrow..stEx")

            TestApproveFlight("testWithNullArgument_shouldThrow..ntEx")

            TestApproveFlight("testWithInvalidApprover_shouldTh..ntEx")

      TestSuite("...flightstate.featuretests.TestDesccheduleFlight")

            TestDesccheduleFlight("testScheduled_shouldEndUpInSc..tate")

            TestDesccheduleFlight("testUnscheduled_shouldThrowIn..stEx")

            TestDesccheduleFlight("testAwaitingApproval_shouldTh..stEx")

      TestSuite("...flightstate.featuretests.TestRequestApproval")

            TestRequestApproval("testScheduledState_shouldThrow..stEx")

            TestRequestApproval("testUnsheduledState_shouldEndU..oval")

            TestRequestApproval("testAwaitingApprovalState_shou..stEx")

      TestSuite("...flightstate.featuretests.TestScheduleFlight")

            TestScheduleFlight("testUnscheduled_shouldEndUpInSc..uled")

            TestScheduleFlight("testScheduledState_shouldThrowI..stEx")

            TestScheduleFlight("testAwaitingApproval_shouldThro..stEx")

TestSuite("...flightstate.featuretests.AllTests")

      TestSuite("...flightstate.featuretests.TestApproveFlight")

            TestApproveFlight("testScheduledState_shouldThrowIn..ReEx")

            TestApproveFlight("testUnsheduled_shouldEndUpInAwai..oval")

            TestApproveFlight("testAwaitingApproval_shouldThrow..stEx")

            TestApproveFlight("testWithNullArgument_shouldThrow..ntEx")

            TestApproveFlight("testWithInvalidApprover_shouldTh..ntEx")

      TestSuite("...flightstate.featuretests.TestDescheduleFlight")

            TestDescheduleFlight("testScheduled_shouldEndUpInSc..tate")

            TestDescheduleFlight("testUnscheduled_shouldThrowIn..stEx")

            TestDescheduleFlight("testAwaitingApproval_shouldTh..stEx")

      TestSuite("...flightstate.featuretests.TestRequestApproval")

            TestRequestApproval("testScheduledState_shouldThrow..stEx")

            TestRequestApproval("testUnsheduledState_shouldEndU..oval")

            TestRequestApproval("testAwaitingApprovalState_shou..stEx")

      TestSuite("...flightstate.featuretests.TestScheduleFlight")

            TestScheduleFlight("testUnscheduled_shouldEndUpInSc..uled")

            TestScheduleFlight("testScheduledState_shouldThrowI..stEx")

            TestScheduleFlight("testAwaitingApproval_shouldThro..stEx")

 

括号外的名称是类的名称;括号内的字符串是从该类创建的对象的名称。按照惯例,要运行的测试方法4的名称用作测试用例对象的名称,而测试套件对象的名称是传递给测试套件对象构造函数的任何字符串。在此示例中,我们使用了测试用例类的完整包和类名。

The name outside the parentheses is the name of the class; the string inside the parentheses is the name of the object created from that class. By convention, the name of the Test Method4 to be run is used as the name of the Testcase Object, and the name of a Test Suite Object is whatever string was passed to the Test Suite Object constructor. In this example we've used the full package and classname of the Testcase Class.

图 19.3.JUnit测试树资源管理器 中显示的一组测试用例对象。

Figure 19.3. A suite of Test Case Objects shown in the JUnit Test Tree Explorer.

图像

测试套件对象

Test Suite Object

当我们有许多测试需要运行时,我们该如何运行测试?

How do we run the tests when we have many tests to run?

我们定义一个实现标准测试接口的集合类,并用它来运行一组相关的测试用例对象。

We define a collection class that implements the standard test interface and use it to run a set of related Testcase Objects.

图像

鉴于我们已经创建了包含测试逻辑的测试方法(第348页) 并将它们放在测试用例类(第 373页) 中,以便我们可以为每个测试构建一个测试用例对象(第 382页),如果能够将这些测试作为单个用户操作来运行,那就太好了。

Given that we have created Test Methods (page 348) containing our test logic and placed them on a Testcase Class (page 373) so we can construct a Testcase Object (page 382) for each test, it would be nice to be able to run these tests as a single user operation.

工作原理

How It Works

我们定义了一个复合[GOF] 测试用例对象(称为测试套件对象)来保存要执行的单个测试用例对象的集合。当我们想一次运行测试套件中的所有测试时,测试运行器第 377页)会要求测试套件对象运行其所有测试。

We define a Composite [GOF] Testcase Object called a Test Suite Object to hold the collection of individual Testcase Objects to execute. When we want to run all tests in the test suite at once, the Test Runner (page 377) asks the Test Suite Object to run all its tests.

我们为什么这样做

Why We Do This

将测试套件视为一等对象,可使测试自动化框架(第 298页)的测试运行器更轻松地操作测试套件中的测试。无论有没有测试套件对象测试运行器都必须保存某种测试用例对象集合(以便我们可以对它们进行迭代、计数等)。当我们使集合变得“智能”时,添加其他用途(例如套件套件)就变得很简单。

Treating test suites as first-class objects makes it easier for the Test Runner of the Test Automation Framework (page 298) to manipulate tests in the test suite. With or without a Test Suite Object, the Test Runner would have to hold some kind of collection of Testcase Objects (so that we could iterate over them, count them, and so on). When we make the collection "smart," it becomes a simple matter to add other uses such as the Suite of Suites.

变体:测试用例类套件

要运行单个Testcase 类中的所有测试方法,我们只需为Testcase 类构建一个测试套件对象,并为每个测试方法添加一个Testcase 对象。这样,我们只需将Testcase 类的名称传递给测试运行器,即可运行Testcase 类中的所有测试方法

To run all the Test Methods in a single Testcase Class, we simply build a Test Suite Object for the Testcase Class and add one Testcase Object for each Test Method. This allows us to run all the Test Methods in the Testcase Class simply by passing the name of the Testcase Class to the Test Runner.

变体:套房中的套房

我们可以通过将较小的测试套件组织成树形结构来构建更大的命名测试套件第 592页)。复合模式使测试运行器无法看到这种组织方式,从而使测试运行器能够以与处理简单测试用例类套件或单个测试用例对象完全相同的方式处理套件套件

We can build up larger Named Test Suites (page 592) by organizing smaller test suites into a tree structure. The Composite pattern makes this organization invisible to the Test Runner, allowing it to treat a Suite of Suites exactly the same way it treats a simple Testcase Class Suite or a single Testcase Object.

实施说明

Implementation Notes

作为复合对象,每个测试套件对象都实现与简单测试用例对象相同的接口因此,测试运行器测试套件对象都不需要知道它是否持有对单个测试或整个套件的引用。这使得实现涉及遍历所有测试的任何操作(例如计数、运行和显示)变得更加容易。

As a Composite object, each Test Suite Object implements the same interface as a simple Testcase Object. Thus neither the Test Runner nor the Test Suite Object needs to be aware of whether it is holding a reference to a single test or an entire suite. This makes it easier to implement any operations that involve iterating across all the tests such as counting, running, and displaying.

在对测试套件对象进行任何操作之前,我们必须先构造它。我们可以从以下几个选项中进行选择:

Before we can do anything with our Test Suite Object, we must construct it. We can choose from several options to do so:

变体:测试套件程序

有时我们必须使用不支持对象的编程语言或脚本语言编写代码。假设我们已经编写了许多测试方法,我们需要为测试运行器提供某种方法来查找测试。测试套件程序允许我们通过依次调用每个测试来枚举我们要运行的所有测试。对每个测试的调用都硬编码在测试套件对象的主体中。当然,测试套件程序可以调用其他几个测试套件程序来实现套件套件

Sometimes we have to write code in programming or scripting languages that do not support objects. Given that we have written a number of Test Methods, we need to give the Test Runner some way to find the tests. A Test Suite Procedure allows us to enumerate all the tests we want to run by invoking each test in turn. The calls to each test are hard-coded within the body of the Test Suite Object. Of course, a Test Suite Procedure may call several other Test Suite Procedures to realize a Suite of Suites.

这种方法的主要缺点是它迫使我们进行测试枚举,这既增加了编写测试所需的工作量,也增加了丢失测试的可能性(请参阅第268页的生产错误)。由于我们不将代码视为“数据”,因此我们失去了在运行时操作代码的能力。因此,构建图形测试运行器(请参阅测试运行器)更加困难,因为图形测试运行器具有套件的层次结构(树)视图。

The major disadvantage of this approach is that it forces us into Test Enumeration, which increases both the effort required to write tests and the likelihood of Lost Tests (see Production Bugs on page 268). Because we do not treat our code as "data," we lose the ability to manipulate the code at runtime. As a consequence, it is more difficult to build a Graphical Test Runner (see Test Runner) with a hierarchy (tree) view of our Suite of Suites.

示例:测试套件对象

Example: Test Suite Object

xUnit 家族的大多数成员都实现了测试发现,因此没有太多测试套件对象的示例可供查看。当我们“深入”测试套件对象以显示其包含的测试用例对象时,测试套件对象存在的主要证据出现在测试树资源管理器中(参见测试运行器图 19.4 )。以下是来自 Eclipse 内置的 JUnit图形测试运行器的示例:

Most members of the xUnit family implement Test Discovery, so there isn't much of an example of Test Suite Object to see. The main evidence of the existence of Test Suite Objects appears in the Test Tree Explorer (see Test Runner and Figure 19.4) when we "drill down" into the Test Suite Object to expose the Testcase Objects it contains. Here's an example from the JUnit Graphical Test Runner built into Eclipse:

图 19.4. 在 JUnit 的测试树资源管理器中查看的测试套件对象

Figure 19.4. A Test Suite Object as viewed in JUnit's Test Tree Explorer.

图像

示例:使用测试枚举构建的套件

Example: Suite of Suites Built Using Test Enumeration

下面是使用测试枚举构造套件的示例:

Here is an example of using Test Enumeration to construct a Suite of Suites:

公共类 AllTests {



      公共静态测试套件() {

            TestSuite suite = new TestSuite(“针对所有 JunitTests 进行测试”);

            suite.addTestSuite(

                      com.clrstream.camug.example.test.InvoiceTest.class);

            suite.addTest(com.clrstream.ex7.test.AllTests.suite());

            suite.addTest(com.clrstream.ex8.test.AllTests.suite());

            suite.addTestSuite(com.xunitpatterns.guardassertion.Example.class);

            返回套件;

      }

}

public  class  AllTests  {



      public  static  Test  suite()  {

            TestSuite  suite  =  new  TestSuite("Test  for  allJunitTests");

            suite.addTestSuite(

                      com.clrstream.camug.example.test.InvoiceTest.class);

            suite.addTest(com.clrstream.ex7.test.AllTests.suite());

            suite.addTest(com.clrstream.ex8.test.AllTests.suite());

            suite.addTestSuite(com.xunitpatterns.guardassertion.Example.class);

            return  suite;

      }

}

 

第一行和最后一行添加了从单个测试用例类创建的测试套件对象。中间两行分别调用测试套件工厂来创建另一个套件。我们返回的测试套件对象可能至少有三层深度:

The first and last lines add the Test Suite Objects created from a single Testcase Class. Each of the middle two lines calls the Test Suite Factory for another Suite of Suites. The Test Suite Object we return is likely at least three levels deep:

  1. 返回之前实例化并填充的测试套件对象
  2. The Test Suite Object we instantiated and populated before returning
  3. 两次工厂方法调用返回的测试AllTests 套件对象
  4. The AllTests Test Suite Objects returned by the two calls to factory methods
  5. 每个测试用例类的测试套件对象都聚合到这些测试套件对象中
  6. The Test Suite Objects for each of the Testcase Classes aggregated into those Test Suite Objects

下面的对象树说明了这一点:

This is illustrated in the following tree of objects:

TestSuite("测试所有JunitTests");

      测试套件(“com.clrstream.camug.example.test.InvoiceTest”)

            测试用例(“testInvoice_addLineItem”)

            ...

            测试用例(“testRemoveLineItemsForProduct_oneOfTwo”)

      测试套件(“com.clrstream.ex7.test.AllTests”)

            测试套件(“com.clrstream.ex7.test.TimeDisplayTest”)

                  测试用例(“testDisplayCurrentTime_AtMidnight”)

                  测试用例(“testDisplayCurrentTime_AtOneMinAfterMidnight”)

                  测试用例(“testDisplayCurrentTime_AtOneMinuteBeforeNoon”)

                  测试用例(“testDisplayCurrentTime_AtNoon”)

                  ...

            测试套件(“com.clrstream.ex7.test.TimeDisplaySolutionTest”)

                  测试用例(“testDisplayCurrentTime_AtMidnight”)

                  测试用例(“testDisplayCurrentTime_AtOneMinAfterMidnight”)

                  测试用例(“testDisplayCurrentTime_AtOneMinuteBeforeNoon”)

                  测试用例(“testDisplayCurrentTime_AtNoon”)

                  ...

      测试套件(“com.clrstream.ex8.test.AllTests”)

            测试套件(“com.clrstream.ex8.FlightMgntFacadeTest”)

                  测试用例(“testAddFlight”)

                  测试用例(“testAddFlightLogging”)

                  测试用例(“testRemoveFlight”)

                  测试用例(“testRemoveFlightLogging”)

                  ...

      测试套件(“com.xunitpatterns.guardassertion.Example”)

                  测试用例(“testWithConditionals”)

                  测试用例(“testWithoutConditionals”)

                  ...

TestSuite("Test  for  allJunitTests");

      TestSuite("com.clrstream.camug.example.test.InvoiceTest")

            TestCase("testInvoice_addLineItem")

            ...

            TestCase("testRemoveLineItemsForProduct_oneOfTwo")

      TestSuite("com.clrstream.ex7.test.AllTests")

            TestSuite("com.clrstream.ex7.test.TimeDisplayTest")

                  TestCase("testDisplayCurrentTime_AtMidnight")

                  TestCase("testDisplayCurrentTime_AtOneMinAfterMidnight")

                  TestCase("testDisplayCurrentTime_AtOneMinuteBeforeNoon")

                  TestCase("testDisplayCurrentTime_AtNoon")

                  ...

            TestSuite("com.clrstream.ex7.test.TimeDisplaySolutionTest")

                  TestCase("testDisplayCurrentTime_AtMidnight")

                  TestCase("testDisplayCurrentTime_AtOneMinAfterMidnight")

                  TestCase("testDisplayCurrentTime_AtOneMinuteBeforeNoon")

                  TestCase("testDisplayCurrentTime_AtNoon")

                  ...

      TestSuite("com.clrstream.ex8.test.AllTests")

            TestSuite("com.clrstream.ex8.FlightMgntFacadeTest")

                  TestCase("testAddFlight")

                  TestCase("testAddFlightLogging")

                  TestCase("testRemoveFlight")

                  TestCase("testRemoveFlightLogging")

                  ...

      TestSuite("com.xunitpatterns.guardassertion.Example")

                  TestCase("testWithConditionals")

                  TestCase("testWithoutConditionals")

                  ...

 

请注意,此类不包含任何其他类的子类。它确实需要导入TestSuite并用作测试套件工厂的类。

Note that this class doesn't subclass any other class. It does need to import TestSuite and the classes it is using as Test Suite Factories.

示例:测试套件程序

Example: Test Suite Procedure

在敏捷软件开发的早期,在出现任何敏捷项目管理工具之前,我构建了一套 Excel 电子表格来管理任务和用户故事。为了让生活更简单,我自动执行了一些经常执行的任务,例如按发布和迭代对所有故事进行排序、按迭代和状态对任务进行排序等等。最后,我大胆地编写了一个宏(实际上是一个程序),它可以汇总每个故事所有任务的估计和实际工作量。此时,代码变得有些复杂,维护起来也更具挑战性。特别是,如果排序宏使用的命名范围之一被意外删除,宏就会产生错误。

In the early days of agile software development, before any agile project management tools were available, I built a set of Excel spreadsheets for managing tasks and user stories. To make life simpler, I automated frequently performed tasks such as sorting all stories by release and iteration, sorting tasks by iteration and status, and so on. Eventually, I got bold enough to write a macro (a program, really) that would sum up the estimated and actual effort of all tasks for each story. At this point, the code was becoming somewhat complex and was more challenging to maintain. In particular, if one of the named ranges used by the sorting macros was accidentally deleted, the macro would produce an error.

不幸的是,当时没有适用于 VBA 的 xUnit 框架,因此所有这些工作都是在没有测试作为安全网的情况下完成的(请参阅第 24页)。这是报告宏的主程序。所有输出都写入工作簿中的新工作表。

Unfortunately, there was no xUnit framework for VBA at the time, so all of this work was done without Tests as Safety Net (see page 24). Here is the main program of the reporting macro. All output was written to a new sheet in the workbook.

'主宏



Sub discussActivities()

      调用 VerifyVersionCompatability

      调用 initialize

      调用 SortByActivity



      For row = firstTaskDataRow To lastTaskDataRow

            如果 numberOfNumberlessTasks < MaxNumberlessTasks 则

                  thisActivity =

                        ActiveSheet.Cells(row, TaskActivityColumn).Value



                  如果 thisActivity <> currentActivity 则

                        调用 finalizeCurrentActivityTotals

                        currentActivity = thisActivity

                        调用 initializeCurrentActivityTotals

                  End If



                  调用 accumulateActivityTotals(row)

            Else

                  lastTaskDataRow = row ' 立即结束 For 循环

            End If

      Next row

      调用 cleanUp

End Sub

'Main  Macro



Sub  summarizeActivities()

      Call  VerifyVersionCompatability

      Call  initialize

      Call  SortByActivity



      For  row  =  firstTaskDataRow  To  lastTaskDataRow

            If  numberOfNumberlessTasks  <  MaxNumberlessTasks  Then

                  thisActivity  =

                        ActiveSheet.Cells(row,  TaskActivityColumn).Value



                  If  thisActivity  <>  currentActivity  Then

                        Call  finalizeCurrentActivityTotals

                        currentActivity  =  thisActivity

                        Call  initializeCurrentActivityTotals

                  End  If



                  Call  accumulateActivityTotals(row)

            Else

                  lastTaskDataRow  =  row      '  end  the  For  loop  right  away

            End  If

      Next  row

      Call  cleanUp

End  Sub

 

在没有任何测试或测试自动化框架的情况下,我不得不尽我所能引入某种回归测试。在这种情况下,仅仅能够运行所有宏就已经是一个足够的挑战(也是一次胜利)。如果它们运行完成,那么这比根本不运行宏更能说明我没有破坏任何重大问题。因为 VBA 基于 Visual Basic 5,所以它没有类。因此,我们没有测试用例类,也没有运行时测试用例对象。以下是我的测试调用的各种测试套件程序测试方法的示例:

Without any tests or Test Automation Framework, I had to do what I could to introduce some kind of regression testing. In this case, it was enough of a challenge (and a win) just to be able to exercise all the macros. If they ran to completion, it was a much better indication that I hadn't broken anything major than not running the macros at all. Because VBA is based on Visual Basic 5, it has no classes. Thus we have no Testcase Class and no runtime Testcase Objects. The following is an example of the various Test Suite Procedures and the Test Methods my tests called:

Sub TestAll()

        调用 TestAllStoryMacros

        调用 TestAllTask​​Macros

        调用 TestReportingMacros

        调用 TestToolbarMenus '全部相同

End Sub



Sub TestAllStoryMacros()

        调用 TestActivitySorting

        调用 TestStoryHiding

        调用 ReportSuccess("所有故事宏")

End Sub



Sub TestActivitySorting()

        调用 SortStoriesbyAreaAndNumber

        调用 SortActivitiesByIteration

        调用 SortActivitiesByIterationAndOrder

        调用 SortActivitiesByNumber

        调用 SortActivitiesByPercentDone

End Sub



Sub TestReportingMacros()

        调用 summaryActivities

End Sub

Sub  TestAll()

        Call  TestAllStoryMacros

        Call  TestAllTaskMacros

        Call  TestReportingMacros

        Call  TestToolbarMenus    'All  The  Same

End  Sub



Sub  TestAllStoryMacros()

        Call  TestActivitySorting

        Call  TestStoryHiding

        Call  ReportSuccess("All  Story  Macros")

End  Sub



Sub  TestActivitySorting()

        Call  SortStoriesbyAreaAndNumber

        Call  SortActivitiesByIteration

        Call  SortActivitiesByIterationAndOrder

        Call  SortActivitiesByNumber

        Call  SortActivitiesByPercentDone

End  Sub



Sub  TestReportingMacros()

        Call  summarizeActivities

End  Sub

 

第一个测试套件程序套件套件;第二个测试套件程序相当于单个测试套件对象。第三个Sub是用于执行所有排序宏的测试方法。最后一个使用预建夹具第 429页)Sub执行宏。5summarizeActivities

The first Test Suite Procedure is a Suite of Suites; the second Test Suite Procedure is the equivalent of a single Test Suite Object. The third Sub is the Test Method for exercising all of the sorting macros. The last Sub exercises the summarizeActivities macro using a Prebuilt Fixture (page 429).5

测试发现

Test Discovery

测试运行器如何知道要运行哪些测试?

How does the Test Runner know which tests to run?

测试自动化框架会自动发现属于测试套件的所有测试。

The Test Automation Framework discovers all tests that belong to the test suite automatically.

图像

假设我们在一个或多个测试用例类(第 373页) 上编写了许多测试方法(第 348页),我们需要为测试运行器(第 377页) 提供某种方法来查找测试。测试发现消除了与测试枚举(第 399页)相关的大部分麻烦。

Given that we have written a number of Test Methods (page 348) on one or more Testcase Classes (page 373), we need to give the Test Runner (page 377) some way to find the tests. Test Discovery eliminates most of the hassles associated with Test Enumeration (page 399).

工作原理

How It Works

测试自动化框架(第298页) 使用运行时反射(或编译时知识)来发现属于测试套件的所有测试方法和/或属于套件套件的所有测试套件对象(第 387页) (请参阅测试套件对象)。然后,它构建包含相应测试用例对象(382页) 和其他测试套件对象的测试套件对象,以准备运行所有测试。

The Test Automation Framework (page 298) uses runtime reflection (or compile-time knowledge) to discover all Test Methods that belong to the test suite and/or all Test Suite Objects (page 387) that belong to a Suite of Suites (see Test Suite Object). It then builds up the Test Suite Objects containing the corresponding Testcase Objects (page 382) and other Test Suite Objects in preparation for running all the tests.

何时使用它

When to Use It

只要我们的测试自动化框架支持测试发现,我们就应该使用测试发现。这种模式减少了自动化测试所需的工作量,并大大降低了丢失测试的可能性(请参阅第268页的生产错误)。只有在以下情况下才需要考虑使用测试枚举:(1)我们的框架不支持测试发现时,以及(2)我们希望定义一个命名测试套件第 592页),该套件由从其他测试套件中选择的测试子集6组成,而测试自动化框架不支持测试选择第 403页)。将测试套件枚举(请参阅测试枚举)与测试方法发现结合起来并不罕见;反过来不太常见。

We should use Test Discovery whenever our Test Automation Framework supports it. This pattern reduces the effort required to automate tests and greatly reduces the possibility of Lost Tests (see Production Bugs on page 268). The only times to consider using Test Enumeration are (1) when our framework does not support Test Discovery and (2) when we wish to define a Named Test Suite (page 592) that consists of a subset of tests6 chosen from other test suites and the Test Automation Framework does not support Test Selection (page 403). It is not uncommon to combine Test Suite Enumeration (see Test Enumeration) with Test Method Discovery; the reverse is less common.

实施说明

Implementation Notes

构建测试运行器要执行的套件套件涉及两个步骤。首先,我们必须找到每个测试套件对象中要包含的所有测试方法。其次,我们必须找到测试运行中要包含的所有测试套件对象,尽管不一定按此顺序。这些步骤中的每一个都可以通过测试方法枚举(参见测试枚举)和测试套件枚举手动完成,也可以通过测试方法发现测试用例类发现自动完成。

Building the Suite of Suites to be executed by the Test Runner involves two steps. First, we must find all Test Methods to be included in each Test Suite Object. Second, we must find all Test Suite Objects to be included in the test run, albeit not necessarily in this order. Each of these steps may be done manually via Test Method Enumeration (see Test Enumeration) and Test Suite Enumeration or automatically via Test Method Discovery and Testcase Class Discovery.

变体:测试用例类发现

测试用例类发现是测试自动化框架发现应该执行测试方法发现的测试用例类的过程。一种解决方案是通过子类化测试用例超类第 638页)或实现标记接口 [PJV1]来标记每个测试用例类。.NET 语言和较新版本的 JUnit 中使用的另一种替代方法是使用类属性(例如)或注释(例如)来标识每个测试用例类。还有一种解决方案是将所有测试用例类放入一个公共目录,并将测试运行器或其他程序指向此目录。第四种解决方案是遵循测试用例类命名约定并使用外部程序查找符合此命名模式的所有文件。无论我们选择哪种方式执行此任务,一旦发现测试用例类,我们就可以继续进行测试方法发现测试方法枚举。"[Test  Fixture]""@Testcase"

Testcase Class Discovery is the process by which the Test Automation Framework discovers the Testcase Classes on which it should do Test Method Discovery. One solution involves tagging each Testcase Class by subclassing a Testcase Superclass (page 638) or implementing a Marker Interface [PJV1]. Another alternative, used in the .NET languages and newer versions of JUnit, is to use a class attribute (e.g., "[Test  Fixture]") or annotation (e.g., "@Testcase") to identify each Testcase Class. Yet another solution is to put all Testcase Classes into a common directory and point the Test Runner or some other program at this directory. A fourth solution is to follow a Testcase Class naming convention and use an external program to find all files matching this naming pattern. Whichever way we choose to perform this task, once a Testcase Class has been discovered we can proceed to either Test Method Discovery or Test Method Enumeration.

变化:测试方法发现

测试方法发现涉及为测试自动化框架提供一种方法来发现测试用例类中的测试方法。有两种基本方法可以表明测试用例类的方法是测试方法。更传统的方法是使用测试方法命名约定,例如“以‘test’开头”。然后,测试自动化框架遍历测试用例类的所有方法,选择以字符串“test”开头的方法(例如),并调用单参数构造函数来为该测试方法创建测试用例对象。另一种替代方法是使用方法属性(例如)或注释(例如)来标识每个测试方法,这种方法在 .NET 语言和较新版本的 JUnit 中使用。testCounters"[Test]""@Test"

Test Method Discovery involves providing a way for the Test Automation Framework to discover the Test Methods in our Testcase Classes. There are two basic ways to indicate that a method of a Testcase Class is a Test Method. The more traditional approach is to use a Test Method naming convention such as "starts with 'test'." The Test Automation Framework then iterates over all methods of the Testcase Class, selects those that start with the string "test" (e.g., testCounters), and calls the one-argument constructor to create the Testcase Object for that Test Method. The other alternative, which is used in the .NET languages and newer versions of JUnit, is to use a method attribute (e.g., "[Test]") or annotation (e.g., "@Test") to identify each Test Method.

激励人心的例子

Motivating Example

下面的例子说明了如果我们没有可用的测试发现,每个测试方法执行测试方法枚举所需的代码类型:

The following example illustrates the kind of code that would be required for each Test Method to do Test Method Enumeration if we did not have Test Discovery available:

公共:

      静态 CppUnit::Test *suite()

      {

            CppUnit::TestSuite *suite =

                        新 CppUnit::TestSuite( "ComplexNumberTest" );

            套件>添加测试(

                        新 CppUnit::TestCaller<ComplexNumberTest>(

                                            "testEquality",

                                            &ComplexNumberTest::testEquality ) );

            套件>添加测试(

                    新 CppUnit::TestCaller<ComplexNumberTest>(

                                            "testAddition",

                                                &ComplexNumberTest::testAddition ) );

            返回套件;

      }

public:

      static  CppUnit::Test  *suite()

      {

            CppUnit::TestSuite  *suite  =

                        new  CppUnit::TestSuite(  "ComplexNumberTest"  );

            suite>addTest(

                        new  CppUnit::TestCaller<ComplexNumberTest>(

                                            "testEquality",

                                            &ComplexNumberTest::testEquality  )  );

            suite>addTest(

                    new  CppUnit::TestCaller<ComplexNumberTest>(

                                            "testAddition",

                                                &ComplexNumberTest::testAddition  )  );

            return  suite;

      }

 

此示例来自 CppUnit 早期版本的教程。较新的版本不再需要这种方法。

This example is from the tutorial for an earlier version of CppUnit. Newer versions no longer require this approach.

重构说明

Refactoring Notes

幸运的是,对于现有 xUnit 家族成员的用户来说,xUnit 的发明者意识到了测试发现的重要性。因此,我们所要做的就是遵循他们关于如何识别测试方法的建议。如果我们的 xUnit 版本的开发人员使用了命名约定,我们可能必须执行重命名方法 [Fowler] 重构才能让 xUnit 发现我们的测试方法。如果他们实现了方法属性,我们只需将适当的属性添加到我们的测试方法中。

Luckily for the users of existing xUnit family members, the inventors of xUnit realized the importance of Test Discovery. Therefore all we have to do is follow their advice on how to identify our test methods. If the developers of our xUnit version used a naming convention, we may have to do a Rename Method [Fowler] refactoring to get xUnit to discover our Test Method. If they implemented method attributes, we just add the appropriate attribute to our Test Methods.

示例:测试方法发现(使用方法命名和编译器宏)

Example: Test Method Discovery (Using Method Naming and Compiler Macro)

当编程语言能够将测试作为对象进行管理并调用方法,但无法轻松找到所有用作测试的方法时,我们可能需要给它一点鼓励。较新版本的 CppUnit 提供了一个宏,它可以在编译时查找所有测试方法并生成代码来构建测试套件,如上例所示。以下代码片段触发测试方法发现

When the programming language is capable of managing the tests as objects and invoking the methods but cannot easily find all methods to use as tests, we may need to give it a small push as encouragement to do so. Newer versions of CppUnit provide a macro that finds all Test Methods at compile time and generates the code to build the test suite as illustrated in the previous example. The following code snippet triggers the Test Method Discovery:

CPPUNIT_TEST_SUITE_REGISTRATION(FlightManagementFacadeTest);

CPPUNIT_TEST_SUITE_REGISTRATION(  FlightManagementFacadeTest  );

 

该宏使用方法命名约定来确定哪些方法(“成员函数”)应该通过用 包装每个方法转换为测试用例对象TestCaller,就像我们之前看到的手册示例一样。

This macro uses a method naming convention to determine which methods ("member functions") it should turn into Testcase Objects by wrapping each with a TestCaller, much like in the manual example we saw earlier.

示例:测试方法发现(使用方法命名)

Example: Test Method Discovery (Using Method Naming)

以下示例缺少的代码比包含的代码更引人注目。请注意,没有测试方法添加到测试套件对象的代码。

The following examples are more notable for the code that is missing than for the code that is present. Note that there is no code to add the Test Methods to the Test Suite Object.

在这个 Java 示例中,框架自动运行所有以“test”开头且没有参数的测试方法(总共两个):

In this Java example, the framework automatically runs all test methods that start with "test" and have no arguments (a total of two):

public class TimeDisplayTest extends TestCase {

      public void testDisplayCurrentTime_AtMidnight()

                    throws Exception {

            // 设置 SUT

          TimeDisplay theTimeDisplay = new TimeDisplay();

            // 练习 SUT

            String actualTimeString =

                        theTimeDisplay.getCurrentTimeAsHtmlFragment();

            // 验证结果

            String expectedTimeString =

                      "<span class=\"tinyBoldText\">Midnight</span>";

            assertEquals( "Midnight",

                                expectedTimeString,

                                actualTimeString);

      }



      public void testDisplayCurrentTime_AtOneMinuteAfterMidnight()

                    throws Exception {

            // 设置 SUT

            TimeDisplay actualTimeDisplay = new TimeDisplay();

            // 练习 SUT

            String actualTimeString =

                      actualTimeDisplay.getCurrentTimeAsHtmlFragment();

            // 验证结果

            String expectedTimeString =

                      "<span class=\"tinyBoldText\">12:01 AM</span>";

            断言Equals( "12:01 AM",

                                expectedTimeString,

                                actualTimeString);

      }

}

public  class  TimeDisplayTest  extends  TestCase  {

      public  void  testDisplayCurrentTime_AtMidnight()

                    throws  Exception  {

            //  Set  up  SUT

          TimeDisplay  theTimeDisplay  =  new  TimeDisplay();

            //  Exercise  SUT

            String  actualTimeString  =

                        theTimeDisplay.getCurrentTimeAsHtmlFragment();

            //  Verify  outcome

            String  expectedTimeString  =

                      "<span  class=\"tinyBoldText\">Midnight</span>";

            assertEquals(  "Midnight",

                                expectedTimeString,

                                actualTimeString);

      }



      public  void  testDisplayCurrentTime_AtOneMinuteAfterMidnight()

                    throws  Exception  {

            //  Set  up  SUT

            TimeDisplay  actualTimeDisplay  =  new  TimeDisplay();

            //  Exercise  SUT

            String  actualTimeString  =

                      actualTimeDisplay.getCurrentTimeAsHtmlFragment();

            //  Verify  outcome

            String  expectedTimeString  =

                      "<span  class=\"tinyBoldText\">12:01  AM</span>";

            assertEquals(  "12:01  AM",

                                expectedTimeString,

                                actualTimeString);

      }

}

 

示例:测试方法发现(使用方法属性)

Example: Test Method Discovery (Using Method Attributes)

在这个 C# 示例中,测试用方法属性 [Test] 标记。CsUnit 和 NUnit 都使用这种方式来标识测试方法

In this C# example, the tests are labeled with the method attribute [Test]. Both CsUnit and NUnit use this way of identifying Test Methods.

[测试]

public void testFlightMileage_asKm()

{

      // 设置夹具

      Flight newFlight = new Flight(validFlightNumber);

      newFlight.setMileage(1122);

      // 练习里程转换器

      int actualKilometres = newFlight.getMileageAsKm();

      int expectedKilometres = 1810;

      // 验证结果

      Assert.AreEqual( expectedKilometres, actualKilometres);

}



[测试]

[ExpectedException(typeof(InvalidArgumentException))]

public void testSetMileage_invalidInput_attribute()

{

      // 设置夹具

      Flight newFlight = new Flight(validFlightNumber);

      // 练习 SUT

      newFlight.setMileage(-1122);

}

[Test]

public  void  testFlightMileage_asKm()

{

      //  set  up  fixture

      Flight  newFlight  =  new  Flight(validFlightNumber);

      newFlight.setMileage(1122);

      //  exercise  mileage  translator

      int  actualKilometres  =  newFlight.getMileageAsKm();

      int  expectedKilometres  =  1810;

      //  verify  results

      Assert.AreEqual(  expectedKilometres,  actualKilometres);

}



[Test]

[ExpectedException(typeof(InvalidArgumentException))]

public  void  testSetMileage_invalidInput_attribute()

{

      //  set  up  fixture

      Flight  newFlight  =  new  Flight(validFlightNumber);

      //  exercise  SUT

      newFlight.setMileage(-1122);

}

 

示例:测试用例类发现(使用类属性)

Example: Testcase Class Discovery (Using Class Attributes)

下面是一个使用类属性来向测试运行器标识测试用例类(在 NUnit 中称为“测试装置”)的示例:

Here is an example of using a class attribute to identify a Testcase Class (called a "Test Fixture" in NUnit) to the Test Runner:

[TestFixture]

公共类 SampleTestcase

{

}

[TestFixture]

public  class  SampleTestcase

{

}

 

示例:测试用例类发现(使用公共位置和测试用例超类)

Example: Testcase Class Discovery (Using Common Location and Testcase Superclass)

以下 Ruby 示例.rb在“tests”目录中查找所有带有扩展名的文件,并requires从此文件中提取它们。这会导致 Test::Unit 在每个文件内查找所有测试,因为每个文件中的Testcase 类Test::Unit::TestCase都扩展了。

The following Ruby example finds all files with the .rb extension in the "tests" directory and requires them from this file. This causes Test::Unit to look for all tests in each file because the Testcase Class in each file extends Test::Unit::TestCase.

Dir['tests/*.rb'].each do |each|

      要求每个

结束

Dir['tests/*.rb'].each  do  |each|

      require  each

end

 

返回Dir['tests/*.rb']一个文件集合,该each方法使用包含“require each”的代码块对其进行迭代以实现Testcase Class Discovery。Ruby 解释器和 Test::Unit 通过对每个类执行测试方法发现required来完成这项工作。

The Dir['tests/*.rb'] returns a collection of files over which the each method iterates with the block containing "require each" to implement Testcase Class Discovery. The Ruby interpreter and Test::Unit finish the job by doing Test Method Discovery on each required class.

测试枚举

Test Enumeration

也称为

Also known as

测试套件工厂

Test Suite Factory

测试运行器如何知道要运行哪些测试?

How does the Test Runner know which tests to run?

测试自动化程序手动编写枚举属于测试套件的所有测试的代码。

The test automater manually writes the code that enumerates all tests that belong to the test suite.

图像

假设我们在一个或多个测试用例类(第 373页) 上编写了许多测试方法(第 348页),我们需要为测试运行器(第377页) 提供某种方法来查找测试。当我们缺乏对测试发现(第 393页)的支持时,测试枚举就是我们这样做的方法。

Given that we have written a number of Test Methods (page 348) on one or more Testcase Classes (page 373), we need to give the Test Runner (page 377) some way to find the tests. Test Enumeration is the way we do so when we lack support for Test Discovery (page 393).

工作原理

How It Works

测试自动化程序手动编写代码,枚举属于测试套件的所有测试方法和/或属于套件套件的所有测试套件对象(第387页) (请参阅测试套件对象)。这通常是通过在测试方法枚举测试用例类上或在测试套件枚举的测试套件工厂上实现方法来完成的。suite

The test automater manually writes the code that enumerates all Test Methods that belong to the test suite and/or all Test Suite Objects (page 387) that belong to a Suite of Suites (see Test Suite Object). This is typically done by implementing the method suite either on a Testcase Class for Test Method Enumeration or on a Test Suite Factory for Test Suite Enumeration.

何时使用它

When to Use It

如果我们的测试自动化框架(第 298页) 不支持测试发现,则需要使用测试枚举。当我们希望定义一个命名测试套件(第 592页),该套件由从其他测试套件中选择的测试7 子集组成,并且框架不支持测试选择(第 403页)时,我们也可以选择使用测试枚举。

We need to use Test Enumeration if our Test Automation Framework (page 298) does not support Test Discovery. We can also choose to use Test Enumeration when we wish to define a Named Test Suite (page 592) that consists of a subset of tests7 chosen from other test suites and the framework does not support Test Selection (page 403).

xUnit 家族的许多成员在测试方法级别支持测试发现,但迫使我们在测试用例类级别使用测试枚举。

Many members of the xUnit family support Test Discovery at the Test Method level but force us to use Test Enumeration at the Testcase Class level.

实施说明

Implementation Notes

构建测试运行器要执行的套件套件涉及两个步骤。首先,我们必须找到每个测试套件对象中要包含的所有测试方法。其次,我们必须找到测试运行中要包含的所有测试套件对象,尽管不一定按此顺序。这些步骤中的每一个都可以通过测试方法枚举测试套件枚举手动完成,也可以通过测试方法发现(参见测试发现)和测试用例类发现(参见测试发现)自动完成。手动完成时,我们通常使用返回测试套件对象的“测试套件工厂” 。

Building the Suite of Suites to be executed by the Test Runner involves two steps. First, we must find all Test Methods to be included in each Test Suite Object. Second, we must find all Test Suite Objects to be included in the test run, albeit not necessarily in this order. Each of these steps may be done manually via Test Method Enumeration and Test Suite Enumeration or automatically via Test Method Discovery (see Test Discovery) and Testcase Class Discovery (see Test Discovery). When done manually, we typically use a "Test Suite Factory" that returns the Test Suite Object.

变体:测试套件枚举

xUnit 家族的许多成员都要求我们提供一个测试套件工厂,该工厂可构建顶级套件套件(通常称为“AllTests”),以此来指定我们希望在测试运行中包含哪些测试套件对象。我们通过在工厂类上提供类方法来做到这一点; xUnit 家族的大多数成员都调用此工厂方法[GOF] 。在方法内部,我们使用对方法的调用,例如将每个嵌套的测试套件对象添加到我们正在构建的套件中。suitesuiteaddTest

Many members of the xUnit family require that we provide a Test Suite Factory that builds the top-level Suite of Suites (often called "AllTests") as means to specify which Test Suite Objects we would like to include in a test run. We do so by providing a class method on a factory class; this Factory Method [GOF] is called suite in most members of the xUnit family. Inside the suite method we use calls to methods such as addTest to add each nested Test Suite Object to the suite we are building.

虽然这种方法相当灵活,但它可能会导致测试丢失(请参阅第 268页的生产错误)。另一种方法是让开发工具自动构建AllTests 套件(请参阅命名测试套件)或使用测试运行器自动在文件系统目录中查找所有测试套件。例如,NUnit 提供了一种内置机制,可在程序集级别实现测试用例类发现。我们还可以使用第三方工具(如 Ant)在目录结构中查找所有测试用例类文件。

Although this approach is fairly flexible, it can result in Lost Tests (see Production Bugs on page 268). The alternative is to let the development tools build the AllTests Suite (see Named Test Suite) automatically or to use a Test Runner that finds all test suites in a file system directory automatically. For example, NUnit provides a built-in mechanism that implements Testcase Class Discovery at the assembly level. We can also use third-party tools such as Ant to find all Testcase Class files in a directory structure.

即使在 Java 等静态类型语言中,测试套件工厂(请参阅第 399页的测试枚举)也不需要子类化特定类或实现特定接口。相反,唯一的依赖项是它返回的通用测试套件对象类以及它要求嵌套套件的测试用例类测试套件工厂。

Even in statically typed languages such as Java, the Test Suite Factory (see Test Enumeration on page 399) does not need to subclass a specific class or implement a specific interface. Instead, the only dependencies are on the generic Test Suite Object class it returns and the Testcase Classes or Test Suite Factories it asks for the nested suites.

变体:测试方法枚举

xUnit 家族的许多成员现在都支持测试方法发现。如果我们碰巧使用的版本不支持该功能,则需要找到测试用例类中的所有测试方法,将它们转换为测试用例对象第 382页),然后将它们放入测试套件对象中。我们通过在测试用例类本身上提供类方法(通常称为)来实现测试方法枚举suite

Many members of the xUnit family now support Test Method Discovery. If we happen to be using a version that does not, we need to find all Test Methods in a Testcase Class, turn them into Testcase Objects (page 382), and put them into a Test Suite Object. We implement Test Method Enumeration by providing a class method, typically called suite, on the Testcase Class itself.

构造调用任意方法的对象的能力通常通过测试用例超类第 638页)从测试自动化框架继承,或通过类属性或指令混合。在 xUnit 系列的某些成员中,此可插入行为[SBPP]功能由单独的类提供(参见下面的CppUnit示例)。Include

The capability to construct an object that calls an arbitrary method is often inherited from the Test Automation Framework via a Testcase Superclass (page 638) or mixed in via a class attribute or Include directive. In some members of the xUnit family, this Pluggable Behavior [SBPP] capability is provided by a separate class (see the CppUnit example below).

变体:直接测试方法调用

在纯程序世界中,我们不能将测试方法视为对象或数据项,我们别无选择,只能为每个测试套件手动编写一个测试套件程序(参见测试套件对象)。然后,此程序逐一调用每个测试方法(或其他测试套件程序)。

In the pure procedural world where we cannot treat a Test Method as an object or data item, we have no choice but to hand-code a Test Suite Procedure (see Test Suite Object) for each test suite. This procedure then calls each Test Method (or other Test Suite Procedures) one by one.

示例:CppUnit 中的测试方法枚举

Example: Test Method Enumeration in CppUnit

大多数 xUnit 系列成员的早期版本都要求测试自动化程序手动添加每个测试方法。那些不能使用反射的版本仍然有此要求。以下是使用此方法的旧版 CppUnit 的示例:

Early versions of most xUnit family members required that the test automater add each Test Method manually. Those versions that cannot use reflection still have this requirement. Here is an example from an older version of CppUnit that uses this approach:

公共:

      静态 CppUnit::Test *suite()

      {

            CppUnit::TestSuite *suite =

                        新 CppUnit::TestSuite( "ComplexNumberTest" );

            套件>添加测试(

                      新 CppUnit::TestCaller<ComplexNumberTest>(

                                          "testEquality",

                                          &ComplexNumberTest::testEquality ) );

            套件>添加测试(

                      新 CppUnit::TestCaller<ComplexNumberTest>(

                                          "testAddition",

                                          &ComplexNumberTest::testAddition ) );

            返回套件;

      }

public:

      static  CppUnit::Test  *suite()

      {

            CppUnit::TestSuite  *suite  =

                        new  CppUnit::TestSuite(  "ComplexNumberTest"  );

            suite>addTest(

                      new  CppUnit::TestCaller<ComplexNumberTest>(

                                          "testEquality",

                                          &ComplexNumberTest::testEquality  )  );

            suite>addTest(

                      new  CppUnit::TestCaller<ComplexNumberTest>(

                                          "testAddition",

                                          &ComplexNumberTest::testAddition  )  );

            return  suite;

      }

 

这个例子还说明了 CppUnit 如何用一个类的实例 ( ) 包装每一个测试方法TestCaller,将其转换成一个测试用例对象

This example also illustrates how CppUnit wraps each Test Method with an instance of a class (TestCaller) to turn it into a Testcase Object.

示例:测试方法调用(硬编码)

Example: Test Method Invocation (Hard-Coded)

以下示例来自用 VBA(Visual Basic for Applications,Microsoft Office 产品中使用的宏语言)编写的程序的测试套件,该程序缺少对对象的支持:

The following example is from a test suite for a program written in VBA (Visual Basic for Applications, the macro language used in Microsoft Office products), which lacks support for objects:

Sub TestAllStoryMacros()

        调用 TestActivitySorting

        调用 TestStoryHiding

        调用 ReportSuccess("所有故事宏")

End Sub

Sub  TestAllStoryMacros()

        Call  TestActivitySorting

        Call  TestStoryHiding

        Call  ReportSuccess("All  Story  Macros")

End  Sub

 

示例:测试套件枚举

Example: Test Suite Enumeration

测试自动化框架不支持测试发现或者我们想要定义仅包含部分测试的命名测试套件时,我们可以使用测试套件枚举。

We can use Test Suite Enumeration when the Test Automation Framework does not support Test Discovery or when we want to define a Named Test Suite that includes only a subset of the tests.

使用测试套件枚举来运行所有测试的主要缺点是,如果我们忘记在AllTests Suite中包含新的测试套件,则可能会丢失测试。可以通过注意我们第一次签出代码时运行的测试数量并确保签入之前运行的测试数量增加了我们添加的新测试数量来降低这种风险。

The main drawback of using Test Suite Enumeration for running all tests is the potential for Lost Tests if we forget to include a new test suite in the AllTests Suite. This risk can be reduced by paying attention to the number of tests that were run when we first checked out the code and ensuring that the number run just before check-in goes up by the number of new tests we added.

公共类 AllTests {



      公共静态测试套件() {

            TestSuite suite = new TestSuite(“针对所有 JunitTests 进行测试”);

            //$JUnit-BEGIN$

            suite.addTestSuite(

                      com.clrstream.camug.example.test.InvoiceTest.class);

            suite.addTest(com.clrstream.ex7.test.AllTests.suite());

            suite.addTest(com.clrstream.ex8.test.AllTests.suite());

            suite.addTestSuite(

                      com.xunitpatterns.guardassertion.Example.class);

            //$JUnit-END$

            返回套件;

      }

}

public  class  AllTests  {



      public  static  Test  suite()  {

            TestSuite  suite  =  new  TestSuite("Test  for  allJunitTests");

            //$JUnit-BEGIN$

            suite.addTestSuite(

                      com.clrstream.camug.example.test.InvoiceTest.class);

            suite.addTest(com.clrstream.ex7.test.AllTests.suite());

            suite.addTest(com.clrstream.ex8.test.AllTests.suite());

            suite.addTestSuite(

                      com.xunitpatterns.guardassertion.Example.class);

            //$JUnit-END$

            return  suite;

      }

}

 

在这个例子中,我们利用了 IDE 为我们(重新)生成套件的功能。(每当我们要求 Eclipse 重新生成两个标记注释之间的代码时,它都会重新生成代码。)我们仍然需要记住偶尔重新生成套件,但这种方法对于在没有测试发现的情况下避免丢失测试AllTests大有帮助。

In this example, we take advantage of the IDE's ability to (re)generate the AllTests suite for us. (Eclipse will regenerate the code between the two marker comments whenever we request it to do so.) We still need to remember to regenerate the suite occasionally, but this approach goes a long way toward avoiding Lost Tests in the absence of Test Discovery.

测试选择

Test Selection

测试运行器 如何知道要运行哪些测试?

How does the Test Runner know which tests to run?

测试自动化框架根据测试的属性选择在运行时运行的测试方法。

The Test Automation Framework selects the Test Methods to be run at runtime based on attributes of the tests.

图像

假设我们已经在一个或多个测试用例类(第 373页) 上编写了许多测试方法(第 348页),我们需要为测试运行器(第377页) 提供某种方法来查找这些测试。测试选择是一种动态选择测试子集的方法。

Given that we have written a number of Test Methods (page 348) on one or more Testcase Classes (page 373), we need to give the Test Runner (page 377) some way to find those tests. Test Selection is a way to pick subsets of tests dynamically.

工作原理

How It Works

测试自动化程序通过提供测试选择标准来指定调用测试运行器时要运行的测试子集。这些选择标准可能基于测试用例类测试方法的隐式或显式属性。

The test automater specifies the subset of tests to be run when invoking the Test Runner by providing test selection criteria. These selection criteria may be based on implicit or explicit attributes of the Testcase Classes or Test Methods.

何时使用它

When to Use It

当我们希望运行从其他测试套件中选择的测试子集,并且不想维护使用测试枚举第 399页)构建的单独结构时,我们应该使用测试选择。冒烟测试 [SCM] 套件是一种常见用法;有关其他用途,请参阅命名测试套件第 592页)。

We should use Test Selection when we wish to run a subset of tests chosen from other test suites and we do not want to maintain a separate structure built using Test Enumeration (page 399). A Smoke Test [SCM] suite is a common usage; see Named Test Suite (page 592) for other uses.

实施说明

Implementation Notes

测试选择可以通过从现有的测试套件对象(第 387子集套件(参见命名测试套件)或通过在执行测试套件对象中包含的测试用例对象(第 382跳过其中的某些测试来实现。

Test Selection can be implemented either by creating a Subset Suite (see Named Test Suite) from an existing Test Suite Object (page 387) or by skipping some of the tests within the Test Suite Object as we execute the Testcase Objects (page 382) it contains.

测试发现(第 393页) 和测试枚举一样,测试选择可应用于两个不同的级别:选择测试用例类或选择测试方法测试选择可以内置到测试自动化框架(第 298页) 中,也可以更粗略地作为构建任务的一部分来实施。

As with Test Discovery (page 393) and Test Enumeration, Test Selection can be applied at two different levels: selecting Testcase Classes or selecting Test Methods. Test Selection can be built into the Test Automation Framework (page 298) or it can be implemented more crudely as part of the build task.

变体:测试用例类别选择

我们可以通过多种方式选择要用于测试方法的测试用例类。进行测试用例类选择的最粗略的方法就是根据某些标准将测试用例类放入测试包中。不幸的是,这种策略仅适用于单一测试分类方案,并且可能会降低测试作为文档的价值(参见第23页)。一种更灵活的方法是使用命名约定(例如“包含‘WebServer’”)来仅选择那些验证系统某些部分行为的类。这也在一定程度上限制了它的实用性。

We can select the Testcase Classes to be examined for Test Methods in several ways. The crudest way to do Testcase Class Selection is simply to place the Testcase Classes into test packages based on some criteria. Unfortunately, this strategy works only for a single test classification scheme and is likely to reduce the value of Tests as Documentation (see page 23). A somewhat more flexible approach is to use a naming convention such as "contains 'WebServer'" to select only those classes that verify the behavior of certain parts of the system. This, too, is somewhat constrained in its utility.

实现测试选择最灵活的方式是在测试自动化框架内。我们可以使用类属性(.NET)或注释(Java)来指示测试用例类的特征。同样的技术也可以应用于测试方法级别。

The most flexible way to implement Test Selection is within the Test Automation Framework. We can use class attributes (.NET) or annotations (Java) to indicate characteristics of the Testcase Class. The same technique can also be applied at the Test Method level.

变化:测试方法选择

当作为测试自动化框架的一部分实现时,可以通过指定测试方法所属的“类别”(或类别)来完成测试方法选择。这通常需要方法属性(.NET)或注释(Java)的语言支持。它也可以基于方法名称方案,尽管这种方法不那么灵活,并且需要与测试运行器更紧密地耦合。

When implemented as part of the Test Automation Framework, Test Method Selection can be done by specifying the "category" (or categories) to which a Test Method belongs. This usually requires language support for method attributes (.NET) or annotations (Java). It could also be based on a method name scheme, although this approach is not as flexible and would require tighter coupling to the Test Runner.

示例:使用类属性进行测试用例类选择

Example: Testcase Class Selection Using Class Attributes

以下测试用例类选择示例来自 NUnit。class 属性Category("FastSuite")表示当在测试运行器中指定类别“FastSuite”时,应包含(或排除)此测试用例类中的所有测试。

The following example of Testcase Class Selection is from NUnit. The class attribute Category("FastSuite") indicates that all tests in this Testcase Class should be included (or excluded) when the category "FastSuite" is specified in the Test Runner.

[TestFixture]

[Category("FastSuite")]

public class CategorizedTests

{

      [Test]

      public void testFlightConstructor_OK()

      // 省略方法

}

[TestFixture]

[Category("FastSuite")]

public  class  CategorizedTests

{

      [Test]

      public  void  testFlightConstructor_OK()

      //  Methods  omitted

}

 

示例:使用方法属性选择测试方法

Example: Test Method Selection Using Method Attributes

测试方法选择示例来自 NUnit。方法属性Category("SmokeTest")表示当在测试运行器中指定类别“SmokeTest”时,应包含(或排除)此测试方法

This example of Test Method Selection is from NUnit. The method attribute Category("SmokeTest") indicates that this Test Method should be included (or excluded) when the category "SmokeTest" is specified in the Test Runner.

[测试]

[Category("SmokeTests")]

public void testFlightMileage_asKm()

{

      // 设置夹具

      Flight newFlight = new Flight(validFlightNumber);

      newFlight.setMileage(1122);

      // 锻炼里程转换器

      int actualKilometres = newFlight.getMileageAsKm();

      int expectedKilometres = 1810;

      // 验证结果

      Assert.AreEqual( expectedKilometres, actualKilometres);

}

[Test]

[Category("SmokeTests")]

public  void  testFlightMileage_asKm()

{

      //  set  up  fixture

      Flight  newFlight  =  new  Flight(validFlightNumber);

      newFlight.setMileage(1122);

      //  exercise  mileage  translator

      int  actualKilometres  =  newFlight.getMileageAsKm();

      int  expectedKilometres  =  1810;

      //  verify  results

      Assert.AreEqual(  expectedKilometres,  actualKilometres);

}

 

第 20 章

夹具设置模式

Chapter 20

Fixture Setup Patterns

 

本章中的模式

Patterns in This Chapter

全新装置设置

Fresh Fixture Setup

      

在线设置 408

      

In-line Setup 408

      

委派设置 411

      

Delegated Setup 411

            

创建方法 415

            

Creation Method 415

      

隐式设置 424

      

Implicit Setup 424

共享装置结构

Shared Fixture Construction

      

预建装置 429

      

Prebuilt Fixture 429

      

惰性设置 435

      

Lazy Setup 435

      

套房固定装置设置 441

      

Suite Fixture Setup 441

      

设置装饰器 447

      

Setup Decorator 447

      

链式测试 454

      

Chained Tests 454

在线设置

In-line Setup

我们如何构建 Fresh Fixture?

How do we construct the Fresh Fixture?

每个测试方法通过调用适当的构造函数方法来构建其所需的测试装置,从而创建自己的 Fresh Fixture。

Each Test Method creates its own Fresh Fixture by calling the appropriate constructor methods to build exactly the test fixture it requires.

图像

要执行自动化测试,我们需要一个易于理解且完全确定的文本夹具。我们可以使用Fresh Fixture第 311页)方法来构建一个Minimal Fixture(第302页)以供此测试使用。在每个测试中以内联方式设置测试夹具是最明显的构建方法。

To execute an automated test, we require a text fixture that is well understood and completely deterministic. We can use the Fresh Fixture (page 311) approach to build a Minimal Fixture (page 302) for the use of this one test. Setting up the test fixture on an in-line basis in each test is the most obvious way to build it.

工作原理

How It Works

每个测试方法(第 348页) 都会通过直接调用所需的 SUT 代码来设置自己的测试装置,以构建其所需的测试装置。我们将创建装置的代码(四阶段测试(第 358页) 的第一阶段)放在每个测试方法的顶部。

Each Test Method (page 348) sets up its own test fixture by directly calling whatever SUT code is required to construct exactly the test fixture it requires. We put the code that creates the fixture, the first phase of the Four-Phase Test (page 358), at the top of each Test Method.

何时使用它

When to Use It

当灯具设置逻辑非常简单明了时,我们可以使用内联设置。一旦灯具设置变得复杂,我们就应该考虑使用委托设置第 411页)或隐式设置第 424页)来完成灯具的部分或全部设置。

We can use In-line Setup when the fixture setup logic is very simple and straightforward. As soon as the fixture setup gets at all complex, we should consider using Delegated Setup (page 411) or Implicit Setup (page 424) for part or all of the fixture setup.

当我们编写测试的初稿并且尚未确定夹具设置的哪一部分将在各个测试之间重复时,我们也可以使用内联设置。这是将“红-绿-重构”流程模式应用于测试本身的一个例子。然而,我们在重构测试时需要小心,以确保我们不会以不可察觉的方式破坏测试。

We can also use In-line Setup when we are writing a first draft of tests and haven't yet figured out which part of the fixture setup will be repeated from test to test. This is an example of applying the "Red–Green–Refactor" process pattern to the tests themselves. Nevertheless, we need to be careful when we refactor the tests to ensure that we don't break the tests in ways that are undetectable.

使用内联设置的第三个场合是重构难懂的装置设置代码。第一步可能是对所有创建方法(第415页)和setUp方法使用内联方法 [Fowler] 重构。然后,我们可以尝试使用一系列提取方法 [Fowler] 重构来定义一组新的创建方法,这些方法更能揭示意图且可重用。

A third occasion to use In-line Setup is when refactoring obtuse fixture setup code. A first step may be to use In-line Method [Fowler] refactorings on all Creation Methods (page 415) and the setUp method. Then we can try using a series of Extract Method [Fowler] refactorings to define a new set of Creation Methods that are more intent-revealing and reusable.

实施说明

Implementation Notes

实际上,大多数装置设置逻辑将包含多种风格,比如在隐式设置之上构建的内联设置或与内联设置交错的委托设置

In practice, most fixture setup logic will include a mix of styles, such as In-line Setup building on top of Implicit Setup or Delegated Setup interspersed with In-line Setup.

示例:在线设置

Example: In-line Setup

以下是简单的内联设置示例。每个测试方法执行 SUT 所需的一切都包含在内联中。

Here's an example of simple in-line setup. Everything each Test Method needs for exercising the SUT is included in-line.

public void testStatus_initial() {

      // 在线设置

      Airport flightpartitionAirport = new Airport("Calgary", "YYC");

      Airport destinationAirport = new Airport("Toronto", "YYZ");

      Flight flight = new Flight(flightNumber,

                                                  flightpartitionAirport,

                                                  destinationAirport);

      // 执行 SUT 并验证结果

     assertEquals(FlightState.PROPOSED, flight.getStatus());

      // 拆除:

          // 垃圾收集

}



public void testStatus_cancelled() {

      // 在线设置

      Airport flightpartitionAirport = new Airport("Calgary", "YYC");

      Airport destinationAirport = new Airport("Toronto", "YYZ");

      Flight flight = new Flight( flightNumber,

                                                  flightpartitionAirport,

                                                  destinationAirport);

      flight.cancel(); // 仍为设置的一部分

      // 执行 SUT 并验证结果

     assertEquals(FlightState.CANCELLED, flight.getStatus());

      // 拆除:

          // 垃圾收集

}

public  void  testStatus_initial()  {

      //  in-line  setup

      Airport  departureAirport  =  new  Airport("Calgary",  "YYC");

      Airport  destinationAirport  =  new  Airport("Toronto",  "YYZ");

      Flight  flight  =  new  Flight(flightNumber,

                                                  departureAirport,

                                                  destinationAirport);

      //  exercise  SUT  and  verify  outcome

     assertEquals(FlightState.PROPOSED,  flight.getStatus());

      //  tearDown:

          //        garbage-collected

}



public  void  testStatus_cancelled()  {

      //  in-line  setup

      Airport  departureAirport  =  new  Airport("Calgary",  "YYC");

      Airport  destinationAirport  =  new  Airport("Toronto",  "YYZ");

      Flight  flight  =  new  Flight(  flightNumber,

                                                  departureAirport,

                                                  destinationAirport);

      flight.cancel();  //  still  part  of  setup

      //  exercise  SUT  and  verify  outcome

     assertEquals(FlightState.CANCELLED,  flight.getStatus());

      //  tearDown:

          //        garbage-collected

}

 

重构说明

Refactoring Notes

内联设置通常是重构的起点,而不是最终目标。然而,有时,我们会发现测试太难理解,因为所有事情都在幕后发生,这是一种神秘客人(请参阅第 186页的模糊测试)。在其他时候,我们可能会发现自己在许多测试中修改了先前设置的装置。

In-line Setup is normally the starting point for refactoring, not the end goal. Sometimes, however, we find ourselves with tests that are too hard to understand because of all the stuff happening behind the scenes, which is a form of Mystery Guest (see Obscure Test on page 186). At other times, we may find ourselves modifying the previously setup fixture in many of the tests.

这两种情况都表明,可能需要根据测试类构建的 Fixture 将测试类重构为多个类。首先,我们在代码上使用内联方法重构来生成内联设置。接下来,我们使用提取类 [Fowler] 重构来重新组织测试。最后,我们使用一系列提取方法重构来定义一组更易于理解的 Fixture 设置方法。

Both of these situations are indications it may be time to refactor our test class into multiple classes based on the fixture they build. First, we use an Inline Method refactoring on the code to produce an In-line Setup. Next, we reorganize the tests using an Extract Class [Fowler] refactoring. Finally, we use a series of Extract Method refactorings to define a more understandable set of fixture setup methods.

委派设置

Delegated Setup

我们如何构建 Fresh Fixture?

How do we construct the Fresh Fixture?

每个测试方法通过从测试方法内部调用创建方法来创建自己的新鲜夹具。

Each Test Method creates its own Fresh Fixture by calling Creation Methods from within the Test Methods.

图像

要执行自动化测试,我们需要一个易于理解且完全确定的文本夹具。我们使用Fresh Fixture (第 311页)方法来构建Minimal Fixture(第302页)以用于此测试,并且我们希望避免测试代码重复(第 213页)。

To execute an automated test, we require a text fixture that is well understood and completely deterministic. We are using a Fresh Fixture (page 311) approach to build a Minimal Fixture (page 302) for the use of this one test and we'd like to avoid Test Code Duplication (page 213).

通过委托设置,我们可以重用代码来设置装置,而不会损害“测试即文档”的目标(参见第23)。

Delegated Setup lets us reuse the code to set up the fixture without compromising our goal of Tests as Documentation (see page 23).

工作原理

How It Works

每个测试方法(第 348页) 都会通过调用一个或多个创建方法(第 415页)来设置自己的测试装置,以精确构建其所需的测试装置。为了确保测试即文档,我们使用创建方法构建了一个最小装置,这些创建方法会构建完全成型的对象,以供测试使用。我们努力确保方法调用只传递那些影响 SUT 行为的值,从而向测试读者传达“全局”。

Each Test Method (page 348) sets up its own test fixture by calling one or more Creation Methods (page 415) to construct exactly the test fixture it requires. To ensure Tests as Documentation, we build a Minimal Fixture using Creation Methods that build fully formed objects that are ready for use by the test. We strive to ensure that the method calls will convey the "big picture" to the test reader by passing in only those values that affect the behavior of the SUT.

何时使用它

When to Use It

当我们想要避免由于必须为多个测试设置相似的 Fixture 而导致的测试代码重复,并且想要在测试方法中保持 Fixture 的性质可见时,可以使用委托设置。一个合理的目标是封装设置Fixture 的必要但不相关的步骤,并只留下对理解测试方法中的测试至关重要的步骤和值。此方案可确保多余的内联设置(第 408页) 代码不会掩盖测试的意图,从而帮助我们实现测试即文档。它还通过将创建方法调用的意图揭示名称[SBPP]留在测试方法中来避免神秘客人问题(请参阅第 186页的模糊测试) 。

We can use a Delegated Setup when we want to avoid the Test Code Duplication caused by having to set up similar fixtures for several tests and we want to keep the nature of the fixture visible within the Test Methods. A reasonable goal is to encapsulate the essential but irrelevant steps of setting up the fixture and leave only the steps and values essential to understanding the test within the Test Method. This scheme helps us achieve Tests as Documentation by ensuring that excess In-line Setup (page 408) code does not obscure the intent of the test. It also avoids the Mystery Guest problem (see Obscure Test on page 186) by leaving the Intent-Revealing Name [SBPP] of the Creation Method call within the Test Method.

此外,委托设置允许我们为测试方法使用任何我们想要的组织方案特别是,我们不必将需要相同测试装置的测试方法放入相同的测试用例类(第 373页) 中,而只需重用该方法,就像使用隐式设置(第424setUp页)时一样。此外,委托设置有助于防止脆弱测试(第 239页),因为它将许多与 SUT 的非必要交互从众多测试方法中移出,并移到数量少得多的创建方法主体中,这样更容易维护。

Furthermore, Delegated Setup allows us to use whatever organization scheme we want for our Test Methods. In particular, we are not forced to put Test Methods that require the same test fixture into the same Testcase Class (page 373) just to reuse the setUp method as we would have to when using Implicit Setup (page 424). Furthermore, Delegated Setup helps prevent Fragile Tests (page 239) by moving much of the nonessential interaction with the SUT out of the very numerous Test Methods and into a much smaller number of Creation Method bodies, where it is easier to maintain.

实施说明

Implementation Notes

借助现代重构工具,我们通常可以通过执行简单的提取方法 [Fowler] 重构来创建创建方法的初稿。当我们使用“克隆和调整”编写一组测试时,我们必须注意测试中的夹具设置逻辑中是否存在任何测试代码重复。对于验证逻辑中需要验证的每个对象,我们提取一个创建方法,该方法仅将那些影响测试结果的属性作为参数。

With modern refactoring tools, we can often create the first cut of a Creation Method by performing a simple Extract Method [Fowler] refactoring. As we are writing a set of tests using "clone and twiddle," we must watch for any Test Code Duplication in the fixture setup logic within our tests. For each object that needs to be verified in the verification logic, we extract a Creation Method that takes only those attributes as parameters that affect the outcome of the test.

最初,我们可以将创建方法保留在测试用例类中。但是,如果我们需要与另一个类共享它们,我们可以将创建方法移至抽象测试用例类(请参阅第638页的测试用例超类)或测试助手类(第643页)。

Initially, we can leave the Creation Method on our Testcase Class. If we need to share them with another class, however, we can move the Creation Methods to an Abstract Testcase class (see Testcase Superclass on page 638) or a Test Helper (page 643) class.

激励人心的例子

Motivating Example

假设我们正在测试类的状态模型Flight。在每次测试中,我们需要让航班处于正确的状态。由于航班需要连接至少两个机场,因此我们需要先创建机场,然后才能创建航班。当然,机场通常与城市或州/省相关联。为了让示例易于管理,我们假设我们的机场只需要城市名称和机场代码。

Suppose we are testing the state model of the Flight class. In each test, we need to have a flight in the right state. Because a flight needs to connect at least two airports, we need to create airports before we can create a flight. Of course, airports are typically associated with cities or states/provinces. To keep the example manageable, let's assume that our airports require only a city name and an airport code.

public void testStatus_initial() {

      // 在线设置

      Airport attendance.begin("Calgary", "YYC");

      Airport destinationAirport = new Airport("Toronto", "YYZ");

      Flight flight = new Flight(flightNumber,

                                                  attendance,

                                                  destinationAirport);

      // 执行 SUT 并验证结果

     assertEquals(FlightState.PROPOSED, flight.getStatus());

      // 拆卸

          // 垃圾收集

}



public void testStatus_cancelled() {

      // 在线设置

      Airport attendance.begin("Calgary", "YYC");

      Airport destinationAirport = new Airport("Toronto", "YYZ");

      Flight flight = new Flight( flightNumber,

                                                    attendance,

                                                    destinationAirport);

      flight.cancel(); // 仍为设置的一部分

      // 执行 SUT 并验证结果

     assertEquals(FlightState.CANCELLED, flight.getStatus());

      // 拆卸

          // 垃圾收集

}

public  void  testStatus_initial()  {

      //  in-line  setup

      Airport  departureAirport  =  new  Airport("Calgary",  "YYC");

      Airport  destinationAirport  =  new  Airport("Toronto",  "YYZ");

      Flight  flight  =  new  Flight(flightNumber,

                                                  departureAirport,

                                                  destinationAirport);

      //  exercise  SUT  and  verify  outcome

     assertEquals(FlightState.PROPOSED,  flight.getStatus());

      //  teardown

          //        garbage-collected

}



public  void  testStatus_cancelled()  {

      //  in-line  setup

      Airport  departureAirport  =  new  Airport("Calgary",  "YYC");

      Airport  destinationAirport  =  new  Airport("Toronto",  "YYZ");

      Flight  flight  =  new  Flight(  flightNumber,

                                                    departureAirport,

                                                    destinationAirport);

      flight.cancel();  //  still  part  of  setup

      //  Exercise  SUT  and  verify  outcome

     assertEquals(FlightState.CANCELLED,  flight.getStatus());

      //  teardown

          //        garbage-collected

}

 

这些测试包含相当数量的测试代码重复

These tests contain a fair amount of Test Code Duplication.

重构说明

Refactoring Notes

我们可以使用 Extract Method 重构来重构夹具设置逻辑,将任何频繁重复的代码序列移除到具有 Intent-Revealing Names 的实用程序方法中。但是,我们将调用方法保留在测试中,以便读者可以看到正在执行的操作。测试中保留的方法调用将向测试读者传达“全局”。实用程序方法主体包含与执行意图无关的机制。如果我们需要与另一个Testcase Class共享Delegated Setups,我们可以使用 Pull Up Method [Fowler] 重构将它们移动到Testcase Superclass或使用 Move Method [Fowler] 重构将它们移动到Test Helper类。

We can refactor the fixture setup logic by using an Extract Method refactoring to remove any frequently repeated code sequences into utility methods with Intent-Revealing Names. We leave the calls to the methods in the test, however, so that the reader can see what is being done. The method calls that remain within the test will convey the "big picture" to the test reader. The utility method bodies contain the irrelevant mechanics of carrying out the intent. If we need to share the Delegated Setups with another Testcase Class, we can use either a Pull Up Method [Fowler] refactoring to move them to a Testcase Superclass or a Move Method [Fowler] refactoring to move them to a Test Helper class.

示例:委托设置

Example: Delegated Setup

在这个版本的测试中,我们使用了一种隐藏需要两个机场这一事实的方法,而不是在每个测试方法中创建航班所需的两个机场。我们可以通过重构或立即以这种意图揭示风格编写测试来生成此版本的测试。

In this version of the test, we use a method that hides the fact that we need two airports instead of creating the two airports needed by the flight within each Test Method. We could produce this version of the tests either through refactoring or by writing the test in this intent-revealing style right off the bat.

public void testGetStatus_initial() {

        // 设置

      Flight flight = createAnonymousFlight();

      // 执行 SUT 并验证结果

     assertEquals(FlightState.PROPOSED, flight.getStatus());

      // 拆除

      // 垃圾收集

}



public void testGetStatus_cancelled2() {

      // 设置

      Flight flight = createAnonymousCancelledFlight();

      // 执行 SUT 并验证结果

     assertEquals(FlightState.CANCELLED, flight.getStatus());

      // 拆除

      // 垃圾收集

}

public  void  testGetStatus_initial()  {

        //  setup

      Flight  flight  =  createAnonymousFlight();

      //  exercise  SUT  and  verify  outcome

     assertEquals(FlightState.PROPOSED,  flight.getStatus());

      //  teardown

      //          garbage-collected

}



public  void  testGetStatus_cancelled2()  {

      //  setup

      Flight  flight  =  createAnonymousCancelledFlight();

      //  exercise  SUT  and  verify  outcome

     assertEquals(FlightState.CANCELLED,  flight.getStatus());

      //  teardown

      //          garbage-collected

}

 

这些测试的简单性是通过以下创建方法实现的,它们向测试读者隐藏了“必要但不相关”的步骤:

The simplicity of these tests was made possible by the following Creation Methods, which hide the "necessary but irrelevant" steps from the test reader:

private int uniqueFlightNumber = 2000;



public Flight createAnonymousFlight(){

      Airport flight = new Airport("Calgary", "YYC");

      Airport destinationAirport = new Airport("Toronto", "YYZ");

      Flight flight =

            new Flight( new BigDecimal(uniqueFlightNumber++),

                      flightoutport,

                      destinationAirport);

      返回航班;

}

public Flight createAnonymousCancelledFlight(){

      Flight flight = createAnonymousFlight();

      flight.cancel();

      返回航班;

}

private  int  uniqueFlightNumber  =  2000;



public  Flight  createAnonymousFlight(){

      Airport  departureAirport  =  new  Airport("Calgary",  "YYC");

      Airport  destinationAirport  =  new  Airport("Toronto",  "YYZ");

      Flight  flight  =

            new  Flight(  new  BigDecimal(uniqueFlightNumber++),

                      departureAirport,

                      destinationAirport);

      return  flight;

}

public  Flight  createAnonymousCancelledFlight(){

      Flight  flight  =  createAnonymousFlight();

      flight.cancel();

      return  flight;

}

 

创建方法

Creation Method

我们如何构建 Fresh Fixture ?

How do we construct the Fresh Fixture ?

我们通过调用隐藏在意图揭示名称后面构建可立即使用对象的机制的方法来设置测试装置。

We set up the test fixture by calling methods that hide the mechanics of building ready-to-use objects behind Intent-Revealing Names.

图像

夹具设置通常涉及创建多个对象。在许多情况下,这些对象的细节(即属性值)并不重要,但必须指定以满足每个对象的构造函数方法。在测试的夹具设置部分中包含所有这些不必要的复杂性可能会导致模糊测试第 186页),并且肯定不会帮助我们实现测试即文档(参见第 23页)!

Fixture setup usually involves the creation of a number of objects. In many cases, the details of those objects (i.e., the attribute values) are unimportant but must be specified to satisfy each object's constructor method. Including all of this unnecessary complexity within the fixture setup part of the test can lead to Obscure Tests (page 186) and certainly doesn't help us achieve Tests as Documentation (see page 23)!

如何才能创建一个正确初始化的对象,而不必用内联设置(第408页)来扰乱测试?答案当然是封装这种复杂性。委托设置第 411页)将夹具设置的机制移至其他方法,但将总体控制和协调留在测试本身中。但是要委托给谁呢?创建方法是封装对象创建机制的一种方式,这样无关的细节就不会分散读者的注意力。

How can a properly initialized object be created without having to clutter the test with In-line Setup (page 408)? The answer, of course, is to encapsulate this complexity. Delegated Setup (page 411) moves the mechanics of the fixture setup into other methods but leaves overall control and coordination within the test itself. But what to delegate to? A Creation Method is one way we can encapsulate the mechanics of object creation so that irrelevant details do not distract the reader.

工作原理

How It Works

在编写测试时,我们不必费心询问所需的实用函数是否存在;我们只需使用它!(可以假装我们旁边有一个忠诚的助手,他会快速填写我们调用的任何尚不存在的函数的主体。)我们根据这些具有意图揭示名称[SBPP]的魔法函数编写测试,仅将那些将在断言中验证或会影响测试结果的内容作为参数传递。

As we write tests, we don't bother asking whether a desired utility function exists; we just use it! (It helps to pretend that we have a loyal helper sitting next to us who will quickly fill in the bodies of any functions we call that do not exist as yet.) We write our tests in terms of these magic functions with Intent-Revealing Names [SBPP], passing as parameters only those things that will be verified in the assertions or that should affect the outcome of the test.

一旦我们以这种非常能揭示意图的风格编写了测试,我们就必须实现我们一直在调用的所有魔法函数。创建对象的函数是我们的创建方法;它们封装了对象创建的复杂性。简单的方法调用适当的构造函数,为任何需要但未作为参数提供的内容传递合适的默认值。如果任何构造函数参数是其他对象,则创建方法将首先创建那些依赖的对象,然后再调用构造函数。

Once we've written the test in this very intent-revealing style, we must implement all of the magic functions that we've been calling. The functions that create objects are our Creation Methods; they encapsulate the complexity of object creation. The simple ones call the appropriate constructor, passing it suitable default values for anything needed but not supplied as a parameter. If any of the constructor arguments are other objects, the Creation Method will first create those depended-on objects before calling the constructor.

创建方法可以放置在我们放置测试实用方法(第 599页) 的所有相同位置。像往常一样,决定基于预期的重用范围和创建方法对 SUT API 的依赖性。相关模式是Object Mother(请参阅第 643页的测试助手),它是创建方法测试助手和可选的自动拆卸(第 503页) 的组合。

The Creation Method may be placed in all the same places where we put Test Utility Methods (page 599). As usual, the decision is based on the expected scope of reuse and the Creation Method's dependencies on the API of the SUT. A related pattern is Object Mother (see Test Helper on page 643), which is a combination of Creation Method, Test Helper, and optionally Automated Teardown (page 503).

何时使用它

When to Use It

每当构建一个Fresh Fixture (第 311页) 需要相当大的复杂性并且我们重视测试作为文档时,我们就应该使用创建方法。使用创建方法的另一个关键指标是,我们正在以高度增量的方式构建系统,并且我们预计系统的 API(尤其是对象构造函数)会经常更改。封装有关如何创建 Fixture 对象的知识是SUT API 封装的一个特例(参见测试实用程序方法),它帮助我们避免脆弱测试第 239页)和模糊测试

We should use a Creation Method whenever constructing a Fresh Fixture (page 311) requires significant complexity and we value Tests as Documentation. Another key indicator for using Creation Method is that we are building the system in a highly incremental way and we expect the API of the system (and especially the object constructors) to change frequently. Encapsulating knowledge of how to create a fixture object is a special case of SUT API Encapsulation (see Test Utility Method), and it helps us avoid both Fragile Tests (page 239) and Obscure Tests.

创建方法的主要缺点是它为测试自动化人员创建了另一个需要学习的 API。对于最初的测试开发人员来说,这不是什么大问题,因为他们通常参与构建此 API,但它可以为团队的新成员创建“另一件事”来学习。即便如此,这个 API 应该很容易理解,因为它只是一组以某种方式组织的工厂方法[GOF] 。

The main drawback of a Creation Method is that it creates another API for test automaters to learn. This isn't much of a problem for the initial test developers because they are typically involved in building this API but it can create "one more thing" for new additions to the team to learn. Even so, this API should be pretty easy to understand because it is just a set of Factory Methods [GOF] organized in some way.

如果我们使用的是预构建夹具第 429页),则应使用Finder 方法(请参阅测试实用程序方法)来定位预构建对象。同时,我们仍可以使用创建方法将我们计划修改的可变对象放置在不可变共享夹具之上(请参阅第317页的共享夹具)。

If we are using a Prebuilt Fixture (page 429), we should use Finder Methods (see Test Utility Method) to locate the prebuilt objects. At the same time, we may still use Creation Methods to lay mutable objects that we plan to modify on top of an Immutable Shared Fixture (see Shared Fixture on page 317).

创作方法的几种变体值得探索。

Several variations of Creation Method are worth exploring.

变体:参数化创建方法

虽然创建方法可以(并且通常非常可取)不采用任何参数,但许多测试将需要对创建的对象进行一些自定义。参数化创建方法允许测试传入一些用于创建对象的属性。在这种情况下,我们应该只传递那些预计会影响测试结果的属性(或者我们想要证明不会影响测试结果的属性);否则,我们可能会陷入模糊测试的泥潭。

While it is possible (and often very desirable) for Creation Methods to take no parameters whatsoever, many tests will require some customization of the created object. A Parameterized Creation Method allows the test to pass in some attributes to be used in the creation of the object. In such a case, we should pass only those attributes that are expected to affect (or those we want to demonstrate do not affect) the test's outcome; otherwise, we could be headed down the slippery slope to Obscure Tests.

变体:匿名创作方法

匿名创建方法会自动创建一个不同的生成值(请参阅第 723页的生成值)作为它正在创建的对象的唯一标识符,即使它接收的参数可能不是唯一的。此行为对于避免不可重复的测试(请参阅第 228页的不稳定测试)非常有用,因为它可以确保我们创建的每个对象都是唯一的,即使在多次测试运行中也是如此。如果测试关心要创建对象的某些属性,它可以将它们作为创建方法的参数传递;此行为将匿名创建方法转变为参数化的匿名创建方法

An Anonymous Creation Method automatically creates a Distinct Generated Value (see Generated Value on page 723) as the unique identifier for the object it is creating even though the arguments it receives may not be unique. This behavior is invaluable for avoiding Unrepeatable Tests (see Erratic Test on page 228) because it ensures that every object we create is unique, even across multiple test runs. If the test cares about some attributes of the object to be created, it can pass them as parameters of the Creation Method; this behavior turns the Anonymous Creation Method into a Parameterized Anonymous Creation Method.

变体:参数化匿名创建方法

参数化匿名创建方法是其他几种创建方法变体的组合,我们传入一些属性用于创建对象,但让创建方法为其创建唯一标识符。如果测试不关心任何属性,创建方法也可以采用零参数。

A Parameterized Anonymous Creation Method is a combination of several other variations of Creation Method in that we pass in some attributes to be used in the creation of the object but let the Creation Method create the unique identifier for it. A Creation Method could also take zero parameters if the test doesn't care about any of the attributes.

变体:命名状态到达方法

一些 SUT 本质上是无状态的,这意味着我们可以随时调用任何方法。相反,当 SUT 状态丰富且方法的有效性或行为受 SUT 状态影响时,从每个可能的起始状态测试每个方法很重要。我们可以在单个测试方法(第 348页) 中将一堆这样的测试链接在一起,但这种方法会创建一个Eager 测试(请参阅第224页的断言轮盘)。为此目的,最好使用一系列单一条件测试(参见第45页)。不幸的是,这给我们留下了一个问题:如何在每个测试中设置起始状态,而又不出现大量的测试代码重复第 213页)。

Some SUTs are essentially stateless, meaning we can call any method at any time. By contrast, when the SUT is state-rich and the validity or behavior of methods is affected by the state of the SUT, it is important to test each method from each possible starting state. We could chain a bunch of such tests together in a single Test Method (page 348), but that approach would create an Eager Test (see Assertion Roulette on page 224). It is better to use a series of Single-Condition Tests (see page 45) for this purpose. Unfortunately, that leaves us with the problem of how to set up the starting state in each test without a lot of Test Code Duplication (page 213).

一个明显的解决方案是将所有依赖于相同起始状态的测试放入同一个测试用例类(第373setUp页),并使用隐式设置第 424页)在方法中创建适当状态下的 SUT (每个装置称为测试用例类;参见第 631页)。另一种方法是通过调用命名状态到达方法来使用委托设置;这种方法允许我们选择其他方式来组织我们的测试用例类

One obvious solution is to put all tests that depend on the same starting state into the same Testcase Class (page 373) and to create the SUT in the appropriate state in the setUp method using Implicit Setup (page 424) (called Testcase Class per Fixture; see page 631). The alternative is to use Delegated Setup by calling a Named State Reaching Method; this approach allows us to choose some other way to organize our Testcase Classes.

无论哪种方式,如果设置 SUT 的代码简短明了,就会更容易理解。这就是命名状态到达方法派上用场的地方。通过将创建正确状态的测试对象所需的逻辑封装在一个地方(无论是在Testcase 类还是在Test Helper上),我们可以减少在需要更改将测试对象置于该状态的方式时必须更新的代码量。

Either way, the code that sets up the SUT will be easier to understand if it is short and sweet. That's where a Named State Reaching Method comes in handy. By encapsulating the logic required to create the test objects in the correct state in a single place (whether on the Testcase Class or a Test Helper), we reduce the amount of code we must update if we need to change how we put the test object into that state.

变化:连接方法

假设我们已经有一个测试对象,并且我们想以某种方式修改它。我们发现自己在足够多的测试中执行此任务,以至于只想编写一次此修改代码。在这种情况下,解决方案是附加方法。此变体与原始创建方法模式之间的主要区别在于,我们传入要修改的对象(可能是另一个创建方法返回的对象)和我们想要设置其属性之一的对象;附加方法为我们完成其余工作。

Suppose we already have a test object and we want to modify it in some way. We find ourselves performing this task in enough tests to want to code this modification once and only once. The solution in this case is an Attachment Method. The main difference between this variation and the original Creation Method pattern is that we pass in the object to be modified (one that was probably returned by another Creation Method) and the object we want to set one of its attributes to; the Attachment Method does the rest of the work for us.

实施说明

Implementation Notes

大多数创建方法都是​​通过对现有测试的某些部分进行提取方法 [Fowler] 重构而创建的。当我们以“由外而内”的方式编写测试时,我们假设创建方法已经存在,并在稍后填写方法主体。实际上,我们定义了一种高级语言(参见第41页)来定义我们的装置。然而,还有另一种完全不同的方法来定义创建方法

Most Creation Methods are created by doing an Extract Method [Fowler] refactoring on parts of an existing test. When we write tests in an "outside-in" manner, we assume that the Creation Methods already exist and fill in the method bodies later. In effect, we define a Higher-Level Language (see page 41) for defining our fixtures. Nevertheless, there is another, completely different way to define Creation Methods.

变化:夹具设置的重复使用测试

我们可以通过调用另一个测试方法来设置夹具,以便为我们完成夹具设置。这假设我们可以通过注册表[PEAA]对象或测试用例对象的实例变量(第 382页)访问其他测试创建的夹具。

We can set up the fixture by calling another Test Method to do the fixture setup for us. This assumes that we have some way of accessing the fixture that the other test created, either through a Registry [PEAA] object or through instance variables of the Testcase Object (page 382).

当我们已经有一些测试依赖于其他测试来设置其测试装置,但我们希望降低链式测试(第 454页) 的测试执行顺序变化导致测试失败的可能性时,以这种方式实现创建方法可能比较合适。请注意,测试将运行得更慢,因为每个测试在每次运行时都会调用它所依赖的所有前面的测试,而不是每个测试在每次测试运行中只运行一次。当然,每个测试只需要调用它实际依赖的特定测试,而不是测试套件中的所有测试。如果我们用伪对象(第 551页) 替换了任何缓慢的组件(例如数据库),这种减速不会太明显。

It may be appropriate to implement a Creation Method in this way when we already have tests that depend on other tests to set up their test fixture but we want to reduce the likelihood that a change in the test execution order of Chained Tests (page 454) will cause tests to fail. Mind you, the tests will run more slowly because each test will call all the preceding tests it depends on each time each test is run rather than each test being run only once per test run. Of course, each test needs to call only the specific tests it actually depends on, not all tests in the test suite. This slowdown won't be very noticeable if we have replaced any slow components, such as a database, with a Fake Object (page 551).

与直接从客户端测试方法调用测试方法相比,将测试方法包装在创建方法是一种更好的选择,因为大多数测试方法的命名都基于它们要验证的测试条件,而不是它们留下的(装置)。创建方法让我们可以在客户端测试方法和实现测试方法之间放置一个能揭示意图的良好名称。它还解决了孤立测试(请参阅不稳定测试)问题,因为另一个测试是在调用测试中明确运行的,而不是仅仅假设它已经运行过。这种方案使测试不那么脆弱,更容易理解,但它不能解决交互测试(请参阅不稳定测试)问题:如果我们调用的测试失败并使测试装置处于与我们预期不同的状态,那么我们的测试也可能会失败,即使我们正在测试的功能仍然有效。

Wrapping the Test Method in a Creation Method is a better option than calling the Test Method directly from the client Test Method because most Test Methods are named based on which test condition(s) they verify, not what (fixture) they leave behind. The Creation Method lets us put a nice Intent-Revealing Name between the client Test Method and the implementing Test Method. It also solves the Lonely Test (see Erratic Test) problem because the other test is run explicitly from within the calling test rather than just assuming that it was already run. This scheme makes the test less fragile and easier to understand but it won't solve the Interacting Tests (see Erratic Test) problem: If the test we call fails and leaves the test fixture in a different state than we expected, our test will likely fail as well, even if the functionality we are testing is still working.

激励人心的例子

Motivating Example

在以下示例中,testPurchase测试要求Customer扮演 的角色buyer。 的名字和姓氏buyer与购买行为无关,但却是Customer构造函数的必需参数;我们关心的是Customer的信用评级是否良好("G")以及他或她当前是否活跃。

In the following example, the testPurchase test requires a Customer to fill the role of the buyer. The first and last names of the buyer have no bearing on the act of purchasing, but are required parameters of the Customer constructor; we do care that the Customer's credit rating is good ("G") and that he or she is currently active.

public void testPurchase_firstPurchase_ICC() {

      客户买方 =

            新客户(17, "FirstName", "LastName", "G","ACTIVE");

      // ...

}

public void testPurchase_subsequentPurchase_ICC() {

      客户买方 =

            新客户(18, "FirstName", "LastName", "G","ACTIVE");

      // ...

}

public  void  testPurchase_firstPurchase_ICC()  {

      Customer  buyer  =

            new  Customer(17,  "FirstName",  "LastName",  "G","ACTIVE");

      //  ...

}

public  void  testPurchase_subsequentPurchase_ICC()  {

      Customer  buyer  =

            new  Customer(18,  "FirstName",  "LastName",  "G","ACTIVE");

      //  ...

}

 

在测试中使用构造函数可能会有问题,尤其是当我们逐步构建应用程序时。构造函数参数的每次更改都会迫使我们重新进行大量测试,或者为了测试而费尽周折,使构造函数签名保持向后兼容。

The use of constructors in tests can be problematic, especially when we are building an application incrementally. Every change to the parameters of the constructor will force us to revisit a lot of tests or jump through hoops to keep the constructor signatures backward compatible for the sake of the tests.

重构说明

Refactoring Notes

我们可以使用提取方法重构来删除对构造函数的直接调用。我们可以为新的创建方法赋予一个适当的揭示意图的名称,例如基于我们创建的创建方法createCustomer的样式。

We can use an Extract Method refactoring to remove the direct call to the constructor. We can give the new Creation Method an appropriate Intent-Revealing Name such as createCustomer based on the style of Creation Method we have created.

示例:匿名创建方法

Example: Anonymous Creation Method

Customer在以下示例中,我们现在不再直接调用构造函数,而是使用Customer 创建方法。请注意,夹具设置代码和构造函数之间的耦合已被消除。如果将另一个参数(例如电话号码)添加到Customer构造函数,则只需更新Customer 创建方法以提供默认值;由于封装,夹具设置代码仍与更改无关。

In the following example, instead of making that direct call to the Customer constructor, we now use the Customer Creation Method. Notice that the coupling between the fixture setup code and the constructor has been removed. If another parameter such as phone number is added to the Customer constructor, only the Customer Creation Method must be updated to provide a default value; the fixture setup code remains insulated from the change thanks to encapsulation.

public void testPurchase_firstPurchase_ACM() {

      客户买方 = createAnonymousCustomer();

      // ...

}

public void testPurchase_subsequentPurchase_ACM() {

      客户买方 = createAnonymousCustomer();

      // ...

}

public  void  testPurchase_firstPurchase_ACM()  {

      Customer  buyer  =  createAnonymousCustomer();

      //  ...

}

public  void  testPurchase_subsequentPurchase_ACM()  {

      Customer  buyer  =  createAnonymousCustomer();

      //  ...

}

 

我们将此模式称为匿名创建方法,因为客户的身份并不重要。匿名创建方法可能看起来像这样:

We call this pattern an Anonymous Creation Method because the identity of the customer does not matter. The Anonymous Creation Method might look something like this:

公共客户 createAnonymousCustomer() {

      int uniqueid = getUniqueCustomerId();

      返回新客户(uniqueid,

                                     “FirstName”+uniqueid,

                                     “LastName”+uniqueid,

                                              “G”,“ACTIVE”);

}

public  Customer  createAnonymousCustomer()  {

      int  uniqueid  =  getUniqueCustomerId();

      return  new  Customer(uniqueid,

                                     "FirstName"  +  uniqueid,

                                     "LastName"  +  uniqueid,

                                              "G",  "ACTIVE");

}

 

注意使用不同的生成值来确保每个匿名Customer略有不同,以避免意外创建相同的Customer

Note the use of a Distinct Generated Value to ensure that each anonymous Customer is slightly different to avoid accidentally creating an identical Customer.

示例:参数化创建方法

Example: Parameterized Creation Method

如果我们想要提供一些Customer属性作为参数,我们可以定义一个参数化创建方法:

If we wanted to supply some of the Customer's attributes as parameters, we could define a Parameterized Creation Method:

public void testPurchase_firstPurchase_PCM() {

      客户买方 =

                  createCreditworthyCustomer("FirstName", "LastName");

      // ...

}

public void testPurchase_subsequentPurchase_PCM() {

      客户买方 =

                  createCreditworthyCustomer("FirstName", "LastName");

      // ...

}

public  void  testPurchase_firstPurchase_PCM()  {

      Customer  buyer  =

                  createCreditworthyCustomer("FirstName",  "LastName");

      //  ...

}

public  void  testPurchase_subsequentPurchase_PCM()  {

      Customer  buyer  =

                  createCreditworthyCustomer("FirstName",  "LastName");

      //  ...

}

 

以下是相应的参数化创建方法定义:

Here's the corresponding Parameterized Creation Method definition:

公共客户 createCreditworthyCustomer(

                               String firstName,String lastName){

      int uniqueid = getUniqueCustomerId();

      客户客户=

              新客户(uniqueid,firstName,lastName,“G”,“ACTIVE”);

      客户.setCredit(CreditRating.EXCELLENT);

      客户.approveCredit();

      返回客户;

}

public  Customer  createCreditworthyCustomer(

                               String  firstName,  String  lastName)  {

      int  uniqueid  =  getUniqueCustomerId();

      Customer  customer  =

              new  Customer(uniqueid,firstName,lastName,"G","ACTIVE");

      customer.setCredit(CreditRating.EXCELLENT);

      customer.approveCredit();

      return  customer;

}

 

示例:连接方法

Example: Attachment Method

下面是一个测试示例,该测试使用附件方法关联两个客户,以验证两个客户是否都获得了他们各自赚取或协商的最佳折扣:

Here's an example of a test that uses an Attachment Method to associate two customers to verify that both get the best discount either of them has earned or negotiated:

public void testPurchase_relatedCustomerDiscount_AM() {

      客户买方 =

                  createCreditworthyCustomer("相关", "买方");

      客户折扣持有人 =

                  createCreditworthyCustomer("折扣", "持有人");

      createRelationshipBetweenCustomers(买方, 折扣持有人);

      // ...

}

public  void  testPurchase_relatedCustomerDiscount_AM()  {

      Customer  buyer  =

                  createCreditworthyCustomer("Related",  "Buyer");

      Customer  discountHolder  =

                  createCreditworthyCustomer("Discount",  "Holder");

      createRelationshipBetweenCustomers(  buyer,  discountHolder);

      //  ...

}

 

在幕后,依恋方法会尽一切努力来建立关系:

Behind the scenes, the Attachment Method does whatever it takes to establish the relationship:

private void createRelationshipBetweenCustomers(

                                                          客户买家,

                                                         客户折扣持有人) {

      买家.addToRelatedCustomersList( 折扣持有人 );

      折扣持有人.addToRelatedCustomersList( 买家 );

}

private  void  createRelationshipBetweenCustomers(

                                                          Customer  buyer,

                                                         Customer  discountHolder)  {

      buyer.addToRelatedCustomersList(  discountHolder  );

      discountHolder.addToRelatedCustomersList(  buyer  );

}

 

尽管这个例子相对简单,但是对这个方法的调用仍然比阅读它所包含的两个方法调用更容易理解。

Although this example is relatively simple, the call to this method is still easier to understand than reading both the method calls of which it consists.

示例:重复用于夹具设置的测试

Example: Test Reused for Fixture Setup

我们可以重用其他测试来为我们的测试设置夹具。以下是如何不这样做的示例:

We can reuse other tests to set up the fixture for our test. Here is an example of how not to do it:

私人客户买方;

私人 AccountManager sut = new AccountManager();

私人帐户帐户;



公共 void testCustomerConstructor_SRT() {

      // 练习

      买方 = 新客户(17,“First”,“Last”,“G”,“ACTIVE”);

      // 验证

     assertEquals(“First”,buyer.firstName(),“first”);

      // ...

}

public void testPurchase_SRT() {

      testCustomerConstructor_SRT(); // 留在字段“买方”中

      account = sut.createAccountForCustomer(buyer);

     assertEquals(buyer.name,account.customerName,“cust”);

      // ...

}

private  Customer  buyer;

private  AccountManager  sut  =    new  AccountManager();

private  Account  account;



public  void  testCustomerConstructor_SRT()  {

      //  Exercise

      buyer  =  new  Customer(17,  "First",  "Last",  "G",  "ACTIVE");

      //  Verify

     assertEquals(  "First",  buyer.firstName(),  "first");

      //  ...

}

public  void  testPurchase_SRT()  {

      testCustomerConstructor_SRT();    //  Leaves  in  field  "buyer"

      account  =  sut.createAccountForCustomer(  buyer  );

     assertEquals(  buyer.name,  account.customerName,  "cust");

      //  ...

}

 

此处的问题有两个方面。首先,我们调用的测试方法的名称描述了它验证的内容(例如,名称),而不是它留下的内容(即,字段Customer中的buyer)。其次,测试不返回Customer;它将留在Customer实例变量中。此方案仅因为我们想要重用的测试方法在同一个测试用例类中而有效;如果它在不相关的类中,我们将不得不进行几次后空翻才能访问buyer。实现此目标的更好方法是将此调用封装在创建方法后面:

The problem here is twofold. First, the name of the Test Method we are calling describes what it verifies (e.g., a name) and not what it leaves behind (i.e., a Customer in the buyer field. Second, the test does not return a Customer; it leaves the Customer in an instance variable. This scheme works only because the Test Method we want to reuse is on the same Testcase Class; if it were on an unrelated class, we would have to do a few backflips to access the buyer. A better way to accomplish this goal is to encapsulate this call behind a Creation Method:

私人客户买方;

私人 AccountManager sut = new AccountManager();

私人帐户帐户;



公共 void testCustomerConstructor_RTCM() {

      // 练习

      买方 = 新客户(17,“First”,“Last”,“G”,“ACTIVE”);

      // 验证

     assertEquals(“First”,buyer.firstName(),“first”);

      // ...

}

公共 void testPurchase_RTCM() {

      买方 = createCreditworthyCustomer();

      帐户 = sut.createAccountForCustomer(买方);

     assertEquals(买方.name,account.customerName,“cust”);

      // ...

}

公共客户 createCreditworthyCustomer() {

      testCustomerConstructor_RTCM();

      返回买方;

      // ...

}

private  Customer  buyer;

private  AccountManager  sut  =    new  AccountManager();

private  Account  account;



public  void  testCustomerConstructor_RTCM()  {

      //  Exercise

      buyer  =  new  Customer(17,  "First",  "Last",  "G",  "ACTIVE");

      //  Verify

     assertEquals(  "First",  buyer.firstName(),  "first");

      //  ...

}

public  void  testPurchase_RTCM()  {

      buyer  =  createCreditworthyCustomer();

      account  =  sut.createAccountForCustomer(  buyer  );

     assertEquals(  buyer.name,  account.customerName,  "cust");

      //  ...

}

public  Customer  createCreditworthyCustomer()  {

      testCustomerConstructor_RTCM();

      return  buyer;

      //  ...

}

 

注意到这个测试的可读性提高了多少吗?我们可以看到它buyer来自哪里!这很容易做到,因为两种测试方法都在同一个类上。如果它们在不同的类上,我们的创建方法必须先创建另一个测试用例类的实例,然后才能运行测试。然后它必须找到一种方法来访问买方实例变量,以便它可以将其返回给调用测试方法。

Notice how much more readable this test has become? We can see where the buyer came from! This was easy to do because both Test Methods were on the same class. If they were on different classes, our Creation Method would have to create an instance of the other Testcase Class before it could run the test. Then it would have to find a way to access the buyer instance variable so that it could return it to the calling Test Method.

隐式设置

Implicit Setup

也称为

Also known as

挂钩设置、框架调用设置、共享设置方法

Hooked Setup, Framework-Invoked Setup, Shared Setup Method

我们如何构建 Fresh Fixture?

How do we construct the Fresh Fixture?

我们在该方法中构建了几个测试所共用的测试夹具setUp

We build the test fixture common to several tests in the setUp method.

图像

要执行自动化测试,我们需要一个易于理解且完全确定的文本夹具。我们使用Fresh Fixture第 311页)方法来构建Minimal Fixture(第302页)以供此测试使用。

To execute an automated test, we require a text fixture that is well understood and completely deterministic. We are using a Fresh Fixture (page 311) approach to build the Minimal Fixture (page 302) for the use of this one test.

隐式设置是一种在测试用例类(第 373页) 中为所有测试方法(348重用夹具设置代码的方法。

Implicit Setup is a way to reuse the fixture setup code for all Test Methods (page 348) in a Testcase Class (page 373).

工作原理

How It Works

通过在Testcase Class上的特殊方法中执行测试装置设置,Testcase Class中的所有测试都会创建相同的Fresh Fixtures。该方法在调用每个测试方法之前由测试自动化框架(第 298页)自动调用。这样就可以重用方法中放置的装置设置代码,而无需重用测试装置的同一实例。这种方法称为“隐式”设置,因为对装置设置逻辑的调用在测试方法中并不明确,这与内联设置(第 408页) 和委托设置(第 411页) 不同。setUpsetUpsetUp

All tests in a Testcase Class create identical Fresh Fixtures by doing test fixture setup in a special setUp method on the Testcase Class. The setUp method is called automatically by the Test Automation Framework (page 298) before it calls each Test Method. This allows the fixture setup code placed in the setUp method to be reused without reusing the same instance of the test fixture. This approach is called "implicit" setup because the calls to the fixture setup logic are not explicit within the Test Method, unlike with In-line Setup (page 408) and Delegated Setup (page 411).

何时使用它

When to Use It

当同一测试用例类上的多个测试方法需要相同的Fresh Fixture时,我们可以使用Implicit Setup。如果所有测试方法都需要完全相同的 Fixture,则可以在方法中设置每个测试所需的整个Minimal Fixture 。这种测试方法组织形式称为每个 Fixture 的测试用例类(第631页)。setUp

We can use Implicit Setup when several Test Methods on the same Testcase Class need an identical Fresh Fixture. If all Test Methods need the exact same fixture, then the entire Minimal Fixture needed by each test can be set up in the setUp method. This form of Test Method organization is known as Testcase Class per Fixture (page 631).

测试方法需要不同的 Fixture 时,因为我们使用的是每个特性一个测试用例类(第624页)或每个类一个测试用例类(第617页)方案,使用隐式设置并构建最小 Fixture会更加困难。我们可以只使用该setUp方法设置 Fixture 中不会给其他测试带来任何问题的部分。一个合理的折衷方案是使用隐式设置来设置 Fixture 中必要但不相关的部分,而将 Fixture 中关键(且因测试而异)部分的设置留给各个测试方法。“必要但不相关”的 Fixture 设置的示例包括使用“不关心”值初始化变量和初始化隐藏的“管道”,例如数据库连接。直接影响 SUT 状态的 Fixture 设置逻辑应留给各个测试方法,除非每个测试方法都需要相同的起始状态。

When the Test Methods need different fixtures because we are using a Testcase Class per Feature (page 624) or Testcase Class per Class (page 617) scheme, it is more difficult to use Implicit Setup and still build a Minimal Fixture. We can use the setUp method only to set up the part of the fixture that does not cause any problems for the other tests. A reasonable compromise is to use Implicit Setup to set up the parts of the fixture that are essential but irrelevant and leave the setup of critical (and different from test to test) parts of the fixture to the individual Test Methods. Examples of "essential but irrelevant" fixture setup include initializing variables with "don't care" values and initializing hidden "plumbing" such as database connections. Fixture setup logic that directly affects the state of the SUT should be left to the individual Test Methods unless every Test Method requires the same starting state.

创建新鲜装置最明显的替代方法是内联设置,其中我们把所有的设置逻辑都包含在每个测试方法中而不考虑任何公共代码;还有委托设置,其中我们将所有公共装置设置代码移动到一组创建方法第 415页)中,我们可以在每个测试方法的设置部分中调用这些方法

The obvious alternatives for creating a Fresh Fixture are In-line Setup, in which we include all setup logic within each Test Method without factoring out any common code, and Delegated Setup, in which we move all common fixture setup code into a set of Creation Methods (page 415) that we can call from within the setup part of each Test Method.

隐式设置可消除大量测试代码重复第 213),并有助于防止出现易碎测试第 239),方法是将大量与 SUT 的非必要交互从大量测试中移出,并移至数量少得多、更易于维护的位置。但是,当神秘嘉宾使每个测试使用的测试装置变得不那么明显时,它可能会导致模糊测试第 186如果类中的所有测试实际上不需要相同的测试装置,它也可能导致易碎装置(请参阅易碎测试)。

Implicit Setup removes a lot of Test Code Duplication (page 213) and helps prevent Fragile Tests (page 239) by moving much of the nonessential interaction with the SUT out of the very numerous tests and into a much smaller number of places where it is easier to maintain. It can, however, lead to Obscure Tests (page 186) when a Mystery Guest makes the test fixture used by each test less obvious. It can also lead to a Fragile Fixture (see Fragile Test) if all tests in the class do not really need identical test fixtures.

实施说明

Implementation Notes

隐式设置的主要实现考虑如下:

The main implementation considerations for Implicit Setup are as follows:

  • 我们如何调用 FixturesetUp方法呢?
  • How do we cause the fixture setUp method to be called?
  • 我们怎样拆除这个装置?
  • How do we tear the fixture down?
  • 测试方法如何访问装置?
  • How do the Test Methods access the fixture?
调用设置代码

方法是处理隐式设置的setUp最常见方式;它包括让测试自动化框架在每个测试方法之前调用该方法。严格来说,方法不是隐式装置设置的唯一形式。例如,套件装置设置第 441页)用于设置和拆除共享装置第 317页),该装置由单个测试用例类上的测试方法重用。此外,设置装饰器第 447页)将方法移动到安装在测试套件对象第 387页)和测试运行器第 377页)之间的装饰器[GOF]对象。两者都是隐式设置的形式,因为测试方法中的逻辑并不明确。setUpsetUpsetUpsetUp

A setUp method is the most common way to handle Implicit Setup; it consists of having the Test Automation Framework call the setUp method before each Test Method. Strictly speaking, the setUp method is not the only form of implicit fixture setup. Suite Fixture Setup (page 441), for example, is used to set up and tear down a Shared Fixture (page 317) that is reused by the Test Methods on a single Testcase Class. In addition, Setup Decorator (page 447) moves the setUp method to a Decorator [GOF] object installed between the Test Suite Object (page 387) and the Test Runner (page 377). Both are forms of Implicit Setup because the setUp logic is not explicit within the Test Method.

拆除装置

隐式设置对应的 Fixture 拆卸是隐式拆卸(页516 )。我们在方法中设置的任何内容,setUp如果不能通过自动拆卸(页503 ) 或垃圾收集自动清理,则应在相应的方法中拆除tearDown

The fixture teardown counterpart of Implicit Setup is Implicit Teardown (page 516). Anything that we set up in the setUp method that is not automatically cleaned up by Automated Teardown (page 503) or garbage collection should be torn down in the corresponding tearDown method.

访问装置

测试方法需要能够访问setUp方法中内置的测试装置。当它们在同一方法中使用时,局部变量就足够了。但是,要在setUp方法和测试方法之间进行通信,必须将局部变量更改为实例变量。我们必须小心,不要将它们变为类变量,因为这将导致共享装置的潜在问题。(请参阅第 384页的边栏“始终存在异常”,了解实例变体何时不提供此级别的隔离。)

The Test Methods need to be able to access the test fixture built in the setUp method. When they were used in the same method, local variables were sufficient. To communicate between the setUp method and the Test Method, however, the local variables must be changed into instance variables. We must be careful not to make them class variables as this will result in the potential for a Shared Fixture. (See the sidebar "There's Always an Exception" on page 384 for a description of when instance variations do not provide this level of isolation.)

激励人心的例子

Motivating Example

在下面的示例中,每个测试都需要在两个机场之间创建一次航班。

In the following example, each test needs to create a flight between a pair of airports.

public void testStatus_initial() {

      // 在线设置

      Airport flightpartitionAirport = new Airport("Calgary", "YYC");

      Airport destinationAirport = new Airport("Toronto", "YYZ");

      Flight flight = new Flight(flightNumber,

                                                      flightpartitionAirport,

                                                     destinationAirport);

      // 执行 SUT 并验证结果

     assertEquals(FlightState.PROPOSED, flight.getStatus());

      // 拆卸

          // 垃圾收集

}

public void testStatus_cancelled() {

      // 在线设置

      Airport flightpartitionAirport = new Airport("Calgary", "YYC");

      Airport destinationAirport = new Airport("Toronto", "YYZ");

      Flight flight = new Flight( flightNumber,

                                                   flightpartitionAirport,

                                                   destinationAirport);

      flight.cancel(); // 仍为设置的一部分

      // 执行 SUT 并验证结果

     assertEquals(FlightState.CANCELLED, flight.getStatus());

      // 拆卸

          // 垃圾收集

}

public  void  testStatus_initial()  {

      //  in-line  setup

      Airport  departureAirport  =  new  Airport("Calgary",  "YYC");

      Airport  destinationAirport  =  new  Airport("Toronto",  "YYZ");

      Flight  flight  =  new  Flight(flightNumber,

                                                      departureAirport,

                                                     destinationAirport);

      //  exercise  SUT  and  verify  outcome

     assertEquals(FlightState.PROPOSED,  flight.getStatus());

      //  teardown

          //        garbage-collected

}

public  void  testStatus_cancelled()  {

      //  in-line  setup

      Airport  departureAirport  =  new  Airport("Calgary",  "YYC");

      Airport  destinationAirport  =  new  Airport("Toronto",  "YYZ");

      Flight  flight  =  new  Flight(  flightNumber,

                                                   departureAirport,

                                                   destinationAirport);

      flight.cancel();  //  still  part  of  setup

      //  exercise  SUT  and  verify  outcome

     assertEquals(FlightState.CANCELLED,  flight.getStatus());

      //  teardown

          //        garbage-collected

}

 

重构说明

Refactoring Notes

这些测试包含大量测试代码重复。我们可以通过重构此测试用例类以使用隐式设置来消除这些重复。有两种重构情况需要考虑。

These tests contain a fair amount of Test Code Duplication. We can remove this duplication by refactoring this Testcase Class to use Implicit Setup. There are two refactoring cases to consider.

首先,当我们发现所有测试都在做类似的工作来设置它们的测试装置,但没有共享setUp方法时,我们可以对其中一个测试中的装置设置逻辑进行提取方法 [Fowler] 重构以创建我们的setUp方法。我们还需要将所有局部变量转换为实例变量(字段),这些变量保存对结果装置的引用,直到测试方法可以访问它。

First, when we discover that all tests are doing similar work to set up their test fixtures but are not sharing a setUp method, we can do an Extract Method [Fowler] refactoring of the fixture setup logic in one of the tests to create our setUp method. We will also need to convert any local variables to instance variables (fields) that hold the references to the resulting fixture until the Test Method can access it.

其次,当我们发现测试用例类已经使用该setUp方法构建夹具并且有需要不同夹具的测试时,我们可以使用提取类 [Fowler] 重构将所有需要不同设置方法的测试方法移至不同的类。我们需要确保用于将夹具知识从设置方法传递到测试方法的任何实例变量都与方法一起传输setUp。有时克隆测试用例类并从类的一个或另一个副本中删除每个测试更简单;然后我们可以从每个类中删除不再使用的任何实例变量。

Second, when we discover that a Testcase Class already uses the setUp method to build the fixture and has tests that need a different fixture, we can use an Extract Class [Fowler] refactoring to move all Test Methods that need a different setup method to a different class. We need to ensure any instance variables that are used to convey knowledge of the fixture from the setup method to the Test Methods are transferred along with the setUp method. Sometimes it is simpler to clone the Testcase Class and delete each test from one or the other copy of the class; we can then delete from each class any instance variables that are no longer being used.

示例:隐式设置

Example: Implicit Setup

在这个修改后的示例中,我们将所有常用的装置设置代码移到了Testcase ClasssetUp的方法中。这样就无需在每个测试中重复此代码,并使每个测试更短 — — 这是一件好事。

In this modified example, we have moved all common fixture setup code to the setUp method of our Testcase Class. This avoids the need to repeat this code in each test and makes each test much shorter—which is a good thing.

机场 出发机场;

机场 目的地机场;

航班 航班;

public void setUp() 抛出异常{

      super.setUp();

      出发机场 = new Airport("Calgary", "YYC");

      目的地机场 = new Airport("Toronto", "YYZ");

      BigDecimal 飞行号码 = new BigDecimal("999");

      飞行 = new Flight( 飞行号码 , 出发机场,

                                         目的地机场);

}



public void testGetStatus_initial() {

      // 隐式设置

      // 执行 SUT 并验证结果

     assertEquals(FlightState.PROPOSED, flight.getStatus());

}



public void testGetStatus_cancelled() {

      // 隐式设置部分覆盖

      flight.cancel();

      // 执行 SUT 并验证结果

     assertEquals(FlightState.CANCELLED, flight.getStatus());

}

Airport  departureAirport;

Airport  destinationAirport;

Flight  flight;

public  void  setUp()  throws  Exception{

      super.setUp();

      departureAirport  =  new  Airport("Calgary",  "YYC");

      destinationAirport  =  new  Airport("Toronto",  "YYZ");

      BigDecimal  flightNumber  =  new  BigDecimal("999");

      flight  =  new  Flight(  flightNumber  ,  departureAirport,

                                         destinationAirport);

}



public  void  testGetStatus_initial()  {

      //  implicit  setup

      //  exercise  SUT  and  verify  outcome

     assertEquals(FlightState.PROPOSED,  flight.getStatus());

}



public  void  testGetStatus_cancelled()  {

      //  implicit  setup  partially  overridden

      flight.cancel();

      //  exercise  SUT  and  verify  outcome

     assertEquals(FlightState.CANCELLED,  flight.getStatus());

}

 

这种方法有几个缺点,这是因为我们没有围绕每个 Fixture 的 Testcase Class来组织我们的测试方法。(我们在这里使用每个 Feature 的 Testcase Class 。) Testcase Class上的所有测试方法都必须能够使用相同的 Fixture(至少作为起点),正如示例中第二个测试中部分覆盖的 Fixture 设置所证明的那样。Fixture 在这些测试中也不是很明显。它来自哪里?有什么特别之处吗?我们甚至无法重命名实例变量来更好地传达飞行的性质,因为我们在每个测试中使用它来保存具有不同特性的飞行。flight

This approach has several disadvantages, which arise because we are not organizing our Test Methods around a Testcase Class per Fixture. (We are using Testcase Class per Feature here.) All the Test Methods on the Testcase Class must be able to make do with the same fixture (at least as a starting point), as evidenced by the partially overridden fixture setup in the second test in the example. The fixture is also not very obvious in these tests. Where does the flight come from? Is there anything special about it? We cannot even rename the instance variable to communicate the nature of the flight better because we are using it to hold flights with different characteristics in each test.

预制装置

Prebuilt Fixture

也称为

Also known as

预建上下文、测试平台

Prebuilt Context, Test Bed

我们怎样才能在第一个需要它的测试方法之前构建共享夹具?

How do we cause the Shared Fixture to be built before the first test method that needs it?

我们在运行测试的同时还分别构建了共享装置。

We build the Shared Fixture separately from running the tests.

图像

当我们选择使用共享夹具(第 317页)时,无论是出于方便还是出于必要,我们都需要在使用共享夹具之前创建它。

When we choose to use a Shared Fixture (page 317), whether it be for reasons of convenience or out of necessity, we need to create the Shared Fixture before we use it.

工作原理

How It Works

我们在运行测试套件之前创建 Fixture。我们可以通过多种不同的方式创建 Fixture,稍后我们会讨论。最重要的一点是,我们不需要每次运行测试套件时都构建 Fixture,因为 Fixture 的寿命比构建它的机制和使用它的任何一次测试运行都要长。

We create the fixture sometime before running the test suite. We can create the fixture a number of different ways that we'll discuss later. The most important point is that we don't need to build the fixture each time the test suite is run because the fixture outlives both the mechanism used to build it and any one test run that uses it.

何时使用它

When to Use It

我们可以通过偶尔创建 Fixture 来减少每次运行测试套件时创建共享Fixture的开销。当构建共享 Fixture的成本极高或无法轻松实现自动化时,此模式尤其适用。

We can reduce the overhead of creating a Shared Fixture each time a test suite is run by creating the fixture only occasionally. This pattern is especially appropriate when the cost of constructing the Shared Fixture is extremely high or cannot be automated easily.

由于在测试运行之前需要手动干预第 250页)来(重新)构建 Fixture,我们可能会多次使用同一个 Fixture,这可能会导致共享 Fixture 污染导致的不稳定测试第 228页)。我们可以通过将预构建 Fixture视为不可变共享 Fixture(请参阅共享 Fixture)并为我们计划修改的任何内容构建一个新 Fixture第 311页),来避免这些问题。

Because of the Manual Intervention (page 250) required to (re)build the fixture before the tests are run, we'll probably end up using the same fixture several times, which can lead to Erratic Tests (page 228) caused by shared fixture pollution. We may be able to avoid these problems by treating the Prebuilt Fixture as an Immutable Shared Fixture (see Shared Fixture) and building a Fresh Fixture (page 311) for anything we plan to modify.

预构建 Fixture的替代方案是每次测试运行构建一次的共享 Fixture和新 Fixture。可以使用Suite Fixture Setup (第 441页)、Lazy Setup (第 435页) 或Setup Decorator (第 447页)构建共享 Fixture 。可以使用In-line Setup (第 408页)、Implicit Setup (第 424页) 或Delegated Setup (第411页)构建新 Fixture 。

The alternatives to a Prebuilt Fixture are a Shared Fixture that is built once per test run and a Fresh Fixture. Shared Fixtures can be constructed using Suite Fixture Setup (page 441), Lazy Setup (page 435), or Setup Decorator (page 447). Fresh Fixtures can be constructed using In-line Setup (page 408), Implicit Setup (page 424), or Delegated Setup (page 411).

变体:全球固定装置

全局Fixture是预构建 Fixture的一个特例,我们在多个测试自动化程序之间共享 Fixture。关键区别在于 Fixture 是全局可见的,而不是特定用户的“私有”对象。当我们使用单个共享数据库沙箱(第650页)而不使用某种形式的数据库分区方案(请参阅数据库沙箱)时,最常采用这种模式。

A Global Fixture is a special case of Prebuilt Fixture where we shared the fixture between multiple test automaters. The key difference is that the fixture is globally visible and not "private" to a particular user. This pattern is most commonly employed when we are using a single shared Database Sandbox (page 650) without using some form of Database Partitioning Scheme (see Database Sandbox).

测试本身可以与用于基本预建夹具的测试相同;同样,夹具设置也与预建夹具相同。不同之处在于我们可能遇到的问题类型。由于夹具现在由多个用户共享,每个用户都在不同的 CPU 上运行单独的测试运行器第 377页),我们可能会遇到各种与多处理相关的问题。最常见的问题是测试运行战争(请参阅不稳定测试),我们会看到看似随机的结果。我们可以通过采用某种数据库分区方案或对具有唯一键约束的任何字段使用不同的生成值(请参阅第723页的生成值)来避免这种可能性。

The tests themselves can be the same as those used for a basic Prebuilt Fixture; likewise, the fixture setup is the same as that for a Prebuilt Fixture. What's different here are the kinds of problems we can encounter. Because the fixture is now shared among multiple users, each of whom is running a separate Test Runner (page 377) on a different CPU, we may experience all sorts of multiprocessing-related issues. The most common problem is a Test Run War (see Erratic Test) where we see seemingly random results. We can avoid this possibility by adopting some kind of Database Partitioning Scheme or by using Distinct Generated Values (see Generated Value on page 723) for any fields with unique key constraints.

实施说明

Implementation Notes

测试本身看上去与基本的共享夹具完全相同。不同之处在于夹具的设置方式。测试读取者无法在测试用例类(第373页)、设置装饰器套件夹具设置方法中找到它的任何迹象。相反,夹具设置最有可能通过某种数据库复制操作手动执行,通过使用数据加载器(请参阅第327页的后门操作)或运行数据库填充脚本。在这些后门设置的示例中(请参阅后门操作),我们绕过 SUT 并直接与其数据库交互。(请参阅第336页的侧栏“数据库作为 SUT API? ”了解后门实际上是前门的示例。)另一个选择是使用夹具设置测试用例(请参阅第 454页的链式测试),以手动或定期计划的方式从测试运行器运行。

The tests themselves look identical to a basic Shared Fixture. What's different is how the fixture is set up. The test reader won't be able to find any sign of it either within the Testcase Class (page 373) or in a Setup Decorator or Suite Fixture Setup method. Instead, the fixture setup is most probably performed manually via some kind of database copy operation, by using a Data Loader (see Back Door Manipulation on page 327) or by running a database population script. In these examples of Back Door Setup (see Back Door Manipulation), we bypass the SUT and interact with its database directly. (See the sidebar "Database as SUT API?" on page 336 for an example of when the back door really is a front door.) Another option is to use a Fixture Setup Testcase (see Chained Tests on page 454) run from a Test Runner either manually or on a regular schedule.

另一个区别是Finder 方法(请参见第599页的测试实用程序方法)的实现方式。我们不能仅仅将创建对象的结果存储在类变量或内存中的测试装置注册表中(请参见第643页的测试助手),因为我们不会在测试运行中使用代码设置装置。我们可以使用两种更常用的选项:(1)在构建装置时将装置构建期间生成的唯一标识符存储在持久测试装置注册表(如文件)中,以便Finder 方法稍后可以检索它们;(2)在Finder 方法中对标识符进行硬编码。我们可以在运行时搜索符合Finder 方法条件的对象 / 记录,但这种方法可能会导致不确定的测试(请参见不稳定的测试),因为每次测试运行最终可能会使用来自预建装置的不同对象 / 记录。如果每次测试运行都会修改对象以使其不再满足条件,则此策略可能是一个好主意。尽管如此,它可能会使调试失败的测试变得相当困难,特别是当由于所选对象的其他属性不同而导致失败间歇性发生时。

Another difference is how the Finder Methods (see Test Utility Method on page 599) are implemented. We cannot just store the results of creating the objects in a class variable or an in-memory Test Fixture Registry (see Test Helper on page 643) because we aren't setting the fixture up in code within the test run. Two of the more commonly used options available to us are (1) to store the unique identifiers generated during fixture construction in a persistent Test Fixture Registry (such as a file) as we build the fixture so that the Finder Methods can retrieve them later and (2) to hard-code the identifiers in the Finder Methods. We could search for objects/records that meet the Finder Methods' criteria at runtime, but that approach might result in Nondeterministic Tests (see Erratic Test) because each test run could end up using a different object/record from the Prebuilt Fixture. This strategy may be a good idea if each test run modifies the objects such that they no longer satisfy the criteria. Nevertheless, it may make debugging a failing test rather difficult, especially if the failures occur intermittently because some other attribute of the selected object is different.

激励人心的例子

Motivating Example

以下示例显示了使用Lazy Setup构建共享装置1

The following example shows the construction of a Shared Fixture using Lazy Setup:1

protected void setUp() throws Exception {

      if (sharedFixtureInitialized) {

            return;

      }

      façade = new FlightMgmtFacadeImpl();

      setupStandardAirportsAndFlights();

      sharedFixtureInitialized = true;

}



protected void teaDown() throws Exception {

      // 无法删除任何对象,因为我们不知道

      // 这是否是最后一次测试

}

protected  void  setUp()  throws  Exception  {

      if  (sharedFixtureInitialized)  {

            return;

      }

      facade  =  new  FlightMgmtFacadeImpl();

      setupStandardAirportsAndFlights();

      sharedFixtureInitialized  =  true;

}



protected  void  tearDown()  throws  Exception  {

      //  Cannot  delete  any  objects  because  we  don't  know

      //  whether  this  is  the  last  test

}

 

setupStandardAirports注意方法中的调用setUp。测试通过调用Finder 方法使用此装置,该方法从装置中返回符合特定条件的对象:

Note the call to setupStandardAirports in the setUp method. The tests use this fixture by calling Finder Methods that return objects from the fixture that match certain criteria:

public void testGetFlightsByFromAirport_OneOutboundFlight()

                throws Exception {

      FlightDto outboundFlight = findOneOutboundFlight();

      // 练习系统

      列表 flightsAtOrigin = Facade.getFlightsByOriginAirport(

                                              outboundFlight.getOriginAirportId());

      // 验证结果

      assertOnly1FlightInDtoList( "Flights at origin",

                                                   outboundFlight,

                                                   flightsAtOrigin);

}



public void testGetFlightsByFromAirport_TwoOutboundFlights()

                throws Exception {

      FlightDto[] outboundFlights =

                            findTwoOutboundFlightsFromOneAirport();

      // 练习系统

      列表 flightsAtOrigin = facade.getFlightsByOriginAirport(

                                  outboundFlights[0].getOriginAirportId());

      // 验证结果

      assertExactly2FlightsInDtoList( "Flights at origin",

                                               outboundFlights,

                                               flightsAtOrigin);

}

public  void  testGetFlightsByFromAirport_OneOutboundFlight()

                throws  Exception  {

      FlightDto  outboundFlight  =  findOneOutboundFlight();

      //  Exercise  System

      List  flightsAtOrigin  =  facade.getFlightsByOriginAirport(

                                              outboundFlight.getOriginAirportId());

      //  Verify  Outcome

      assertOnly1FlightInDtoList(  "Flights  at  origin",

                                                   outboundFlight,

                                                   flightsAtOrigin);

}



public  void  testGetFlightsByFromAirport_TwoOutboundFlights()

                throws  Exception  {

      FlightDto[]  outboundFlights  =

                            findTwoOutboundFlightsFromOneAirport();

      //  Exercise  System

      List  flightsAtOrigin  =  facade.getFlightsByOriginAirport(

                                  outboundFlights[0].getOriginAirportId());

      //  Verify  Outcome

      assertExactly2FlightsInDtoList(  "Flights  at  origin",

                                               outboundFlights,

                                               flightsAtOrigin);

}

 

重构说明

Refactoring Notes

将测试用例类标准夹具(第305页)转换为预建夹具的一种方法是执行提取类 [Fowler] 重构,以便夹具设置在一个类中,而测试方法(第348页) 位于另一个类中。当然,我们需要为Finder 方法提供一种方式来确定结构中存在哪些对象或记录,因为我们无法保证任何实例或类变量将弥补夹具构造和夹具使用之间的时间差距。

One way to convert a Testcase Class from a Standard Fixture (page 305) to a Prebuilt Fixture is to do an Extract Class [Fowler] refactoring so that the fixture is set up in one class and the Test Methods (page 348) are located in another class. Of course, we need to provide a way for the Finder Methods to determine which objects or records exist in the structure because we won't be able to guarantee that any instance or class variables will bridge the time gap between fixture construction and fixture usage.

示例:预建夹具测试

Example: Prebuilt Fixture Test

这是包含测试方法的结果测试用例类。请注意,它看起来几乎与基本的共享夹具测试相同。

Here is the resulting Testcase Class that contains the Test Methods. Note that it looks almost identical to the basic Shared Fixture tests.

public void testGetFlightsByFromAirport_OneOutboundFlight()

                  throws Exception {

      FlightDto outboundFlight = findOneOutboundFlight();

      // 练习系统

      列表 flightsAtOrigin = Facade.getFlightsByOriginAirport(

                                          outboundFlight.getOriginAirportId());

      // 验证结果

      assertOnly1FlightInDtoList( "Flights at origin",

                                                  outboundFlight,

                                                 flightsAtOrigin);

}



public void testGetFlightsByFromAirport_TwoOutboundFlights()

                throws Exception {

      FlightDto[] outboundFlights =

            findTwoOutboundFlightsFromOneAirport();

      // 练习系统

      列表 flightsAtOrigin = facade.getFlightsByOriginAirport(

                                   outboundFlights[0].getOriginAirportId());

      // 验证结果

      assertExactly2FlightsInDtoList( "Flights at origin",

                                                          outboundFlights,

                                                          flightsAtOrigin);

}

public  void  testGetFlightsByFromAirport_OneOutboundFlight()

                  throws  Exception  {

      FlightDto  outboundFlight  =  findOneOutboundFlight();

      //  Exercise  System

      List  flightsAtOrigin  =  facade.getFlightsByOriginAirport(

                                          outboundFlight.getOriginAirportId());

      //  Verify  Outcome

      assertOnly1FlightInDtoList(  "Flights  at  origin",

                                                  outboundFlight,

                                                 flightsAtOrigin);

}



public  void  testGetFlightsByFromAirport_TwoOutboundFlights()

                throws  Exception  {

      FlightDto[]  outboundFlights  =

            findTwoOutboundFlightsFromOneAirport();

      //  Exercise  System

      List  flightsAtOrigin  =  facade.getFlightsByOriginAirport(

                                   outboundFlights[0].getOriginAirportId());

      //  Verify  Outcome

      assertExactly2FlightsInDtoList(  "Flights  at  origin",

                                                          outboundFlights,

                                                          flightsAtOrigin);

}

 

不同之处在于如何设置装置以及如何实现Finder 方法。

What's different is how the fixture is set up and how the Finder Methods are implemented.

示例:Fixture Setup 测试用例

Example: Fixture Setup Testcase

我们可能会发现使用 xUnit 设置预建夹具非常方便。如果我们已经定义了适当的创建方法第 415页)或构造函数,并且我们有一种轻松将对象持久化到数据库沙箱中的方法,那么这很容易做到。在下面的例子中,我们从setUp方法中调用与上一个示例相同的方法,只是现在该方法位于夹具设置测试用例setUp的方法中,每当我们想要重新生成预建夹具时都可以运行该方法:

We may find it to be convenient to set up our Prebuilt Fixture using xUnit. This is simple to do if we already have the appropriate Creation Methods (page 415) or constructors already defined and we have a way to easily persist the objects into the Database Sandbox. In the following example, we call the same method as in the previous example from the setUp method, except that now the method lives in the setUp method of a Fixture Setup Testcase that can be run whenever we want to regenerate the Prebuilt Fixture:

public class FlightManagementFacadeSetupTestcase

                extends AbstractFlightManagementFacadeTestCase {

      public FlightManagementFacadeSetupTestcase(String name) {

            super(name);

      }



      protected void setUp() throws Exception {

            façade = new FlightMgmtFacadeImpl();

            helper = new FlightManagementTestHelper();

            setupStandardAirportsAndFlights();

            saveFixtureInformation();

      }



      protected void teaDown() throws Exception {

            // 保留预建的 Fixture 以供以后使用

      }

}

public  class  FlightManagementFacadeSetupTestcase

                extends  AbstractFlightManagementFacadeTestCase  {

      public  FlightManagementFacadeSetupTestcase(String  name)  {

            super(name);

      }



      protected  void  setUp()  throws  Exception  {

            facade  =  new  FlightMgmtFacadeImpl();

            helper  =  new  FlightManagementTestHelper();

            setupStandardAirportsAndFlights();

            saveFixtureInformation();

      }



      protected  void  tearDown()  throws  Exception  {

            //  Leave  the  Prebuilt  Fixture  for  later  use

      }

}

 

请注意,此测试用例类上没有测试方法,并且方法是空的。这里我们只想进行设置— 不做其他任何事情。tearDown

Note that there are no Test Methods on this Testcase Class and the tearDown method is empty. Here we want to do only the setup—nothing else.

一旦创建了对象,我们就使用对该方法的调用将信息保存到数据库中,saveFixtureInformation;该方法会持久保存对象并将各种键保存在文件中,以便我们可以重新加载它们以供后续实际测试运行使用。这种方法避免了将夹具知识硬编码到测试方法测试实用程序方法中的需要。为了节省篇幅,我将省略有关如何找到“脏”对象并保存关键信息的细节;有多种方法可以处理此任务,任何这些策略都可以。

Once we created the objects, we saved the information to the database using the call to saveFixtureInformation; this method persists the objects and saves the various keys in a file so that we can reload them for use from the subsequent real test runs. This approach avoids the need to hard-code knowledge of the fixture into Test Methods or Test Utility Methods. In the interest of space, I'll spare you the details of how we find the "dirty" objects and save the key information; there is more than one way to handle this task and any of these tactics will suffice.

示例:使用数据填充脚本预建夹具设置

Example: Prebuilt Fixture Setup Using a Data Population Script

在数据库沙箱中构建预建夹具的方法与编程语言一样多— 从 SQL 脚本到 Pearl 和 Ruby 程序,应有尽有。这些脚本可以包含数据,也可以从一组平面文件中读取数据。我们甚至可以将“黄金”数据库的内容复制到我们的数据库沙箱中。我将把它留给您作为练习,让您找出最适合您特定情况的方法。

There are as many ways to build a Prebuilt Fixture in a Database Sandbox as there are programming languages—everything from SQL scripts to Pearl and Ruby programs. These scripts can contain the data or they can read the data from a collection of flat files. We can even copy the contents of a "golden" database into our Database Sandbox. I'll leave it as an exercise for you to figure out what's most appropriate in your particular circumstance.

惰性设置

Lazy Setup

我们怎样才能在第一个需要它的测试方法之前构建共享夹具?

How do we cause the Shared Fixture to be built before the first test method that needs it?

我们使用夹具的延迟初始化在第一次需要它的测试中创建它。

We use Lazy Initialization of the fixture to create it in the first test that needs it.

图像

共享夹具第 317)通常用于通过减少需要创建复杂夹具的次数来加快测试执行速度。不幸的是,依赖于其他测试来设置夹具的测试无法自行运行;它是一个孤独测试(请参阅第 228页的不稳定测试

Shared Fixtures (page 317) are often used to speed up test execution by reducing the number of times a complex fixture needs to be created. Unfortunately, a test that depends on other tests to set up the fixture cannot be run by itself; it is a Lonely Test (see Erratic Test on page 228)

如果尚未设置装置,我们可以让每个测试使用Lazy Setup来设置装置,从而避免这个问题。

We can avoid this problem by having each test use Lazy Setup to set up the fixture if it is not already set up.

工作原理

How It Works

我们使用延迟初始化[SBPP]在第一个需要它的测试中构建 Fixture,然后将 Fixture 的引用存储在每个测试都可以访问的类变量中。所有后续运行的测试都会发现 Fixture 已经创建,并且可以重复使用它,从而避免了重新构建 Fixture 的工作。

We use Lazy Initialization [SBPP] to construct the fixture in the first test that needs it and then store a reference to the fixture in a class variable that every test can access. All subsequently run tests will discover that the fixture is already created and that they can reuse it, thereby avoiding the effort of constructing the fixture anew.

何时使用它

When to Use It

每当我们需要创建一个共享夹具,但仍希望能够单独运行每个测试时,我们都可以 使用惰性设置。如果夹具没有必要被拆除,我们也可以使用惰性设置来代替其他技术,如设置装饰器(第447页) 和套件夹具设置(第441页)。例如,当我们使用一个可以通过垃圾收集拆卸(第500页)拆除的夹具时,我们可以使用惰性设置。当我们对所有数据库键使用不同的生成值(请参阅第723页的生成值),并且不担心在每次测试后留下多余的记录时,我们也可以使用惰性设置;增量断言(第 485页) 使这种方法成为可能。

We can use Lazy Setup whenever we need to create a Shared Fixture yet still want to be able to run each test by itself. We can also use Lazy Setup instead of other techniques such as Setup Decorator (page 447) and Suite Fixture Setup (page 441) if it is not crucial that the fixture be torn down. For example, we could use Lazy Setup when we are using a fixture that can be torn down by Garbage-Collected Teardown (page 500). We might also use Lazy Setup when we are using Distinct Generated Values (see Generated Value on page 723) for all database keys and aren't worried about leaving extra records lying around after each test; Delta Assertions (page 485) make this approach possible.

延迟设置的主要缺点是,虽然很容易发现我们正在运行第一个测试,需要构建装置,但很难确定我们正在运行最后一个测试,装置应该被销毁。xUnit测试自动化框架系列(第 298页) 的大多数成员都没有提供任何方法来确定这一事实,除非对整个测试套件使用设置装饰器。xUnit 系列的一些成员支持套件装置设置(NUnit、VbUnit 和 JUnit 4.0 及更新版本,仅举几例),它为测试用例类(第 373setUp/tearDown页) 提供了“书挡” 。不幸的是,如果我们用 Ruby、Python 或 PLSQL 编写测试,这种能力就帮不上忙了!

The major disadvantage of Lazy Setup is the fact that while it is easy to discover that we are running the first test and need to construct the fixture, it is difficult to determine that we are running the last test and the fixture should be destroyed. Most members of the xUnit family of Test Automation Frameworks (page 298) do not provide any way to determine this fact other than by using a Setup Decorator for the entire test suite. A few members of the xUnit family support Suite Fixture Setup (NUnit, VbUnit, and JUnit 4.0 and newer, to name a few), which provides setUp/tearDown "bookends" for a Testcase Class (page 373). Unfortunately, this ability won't help us if we are writing our tests in Ruby, Python, or PLSQL!

某些 IDE 和测试运行器第 377页)会在每次运行测试套件时自动重新加载我们的类。这会导致原始类变量超出范围,并且在运行新版本的类之前,夹具将被垃圾回收。在这些情况下,使用Lazy Setup可能不会产生任何负面后果。

Some IDEs and Test Runners (page 377) automatically reload our classes every time the test suite is run. This causes the original class variable to go out of scope, and the fixture will be garbage-collected before the new version of the class is run. In these cases there may be no negative consequence of using Lazy Setup.

预建夹具(第429页)是另一种为每次测试运行设置共享夹具的替代方案。如果夹具被某些测试破坏,则使用预建夹具可能会导致不可重复的测试(请参阅不稳定测试)。

A Prebuilt Fixture (page 429) is another alternative to setting up the Shared Fixture for each test run. Its use can lead to Unrepeatable Tests (see Erratic Test) if the fixture is corrupted by some of the tests.

实施说明

Implementation Notes

因为Lazy Setup只对共享 Fixtures有意义,所以Lazy Setup带有与共享 Fixtures相同的负担。

Because Lazy Setup makes sense only with Shared Fixtures, Lazy Setup carries all the same baggage that comes with Shared Fixtures.

通常,延迟设置用于构建共享夹具,供单个测试用例类使用。夹具的引用保存在类变量中。如果我们想在多个测试用例类之间共享夹具,事情会变得有点棘手。我们可以将延迟初始化逻辑和类变量都移动到测试用例超类第 638页),但前提是我们的语言支持类变量的继承。另一种选择是将逻辑和变量移动到测试助手第 643页)。

Normally, Lazy Setup is used to build a Shared Fixture to be used by a single Testcase Class. The reference to the fixture is held in a class variable. Things get a bit trickier if we want to share the fixture across several Testcase Classes. We could move both the Lazy Initialization logic and the class variable to a Testcase Superclass (page 638) but only if our language supports inheritance of class variables. The other alternative is to move the logic and variables to a Test Helper (page 643).

当然,我们可以使用引用计数等方法来了解所有测试方法(第 348页) 是否都已运行。挑战在于要知道测试套件对象(第 387页) 中有多少个测试用例对象(第 382页),以便我们可以将此数字与方法被调用的次数进行比较。我从未见过有人这样做,所以我不会称其为模式!向测试运行器添加逻辑以在测试套件对象级别调用方法相当于实现套件装置设置tearDowntearDown

Of course, we could use an approach such as reference counting as a way to know whether all Test Methods (page 348) have run. The challenge would be to know how many Testcase Objects (page 382) are in the Test Suite Object (page 387) so that we can compare this number with the number of times the tearDown method has been called. I have never seen anyone do this so I won't call it a pattern! Adding logic to the Test Runner to invoke a tearDown method at the Test Suite Object level would amount to implementing Suite Fixture Setup.

激励人心的例子

Motivating Example

在这个例子中,我们为每个测试用例对象构建了一个新的装置:

In this example, we have been building a new fixture for each Testcase Object:

public void testGetFlightsByFromAirport_OneOutboundFlight()

                      throws Exception {

      setupStandardAirportsAndFlights();

      FlightDto outboundFlight = findOneOutboundFlight();

      // 练习系统

      列表 flightsAtOrigin = Facade.getFlightsByOriginAirport(

                                      outboundFlight.getOriginAirportId());

      // 验证结果

      assertOnly1FlightInDtoList( "Flights at origin",

                                                              outboundFlight,

                                                              flightsAtOrigin);

}

public void testGetFlightsByFromAirport_TwoOutboundFlights()

                      throws Exception {

      setupStandardAirportsAndFlights();

      FlightDto[] outboundFlights =

                            findTwoOutboundFlightsFromOneAirport();

      // 练习系统

      列表 flightsAtOrigin = Facade.getFlightsByOriginAirport(

                                      outboundFlights[0].getOriginAirportId());

      // 验证结果

      assertExactly2FlightsInDtoList( "出发地航班",

                                                                      outboundFlights,

                                                                      flightsAtOrigin);

}

public  void  testGetFlightsByFromAirport_OneOutboundFlight()

                      throws  Exception  {

      setupStandardAirportsAndFlights();

      FlightDto  outboundFlight  =  findOneOutboundFlight();

      //  Exercise  System

      List  flightsAtOrigin  =  facade.getFlightsByOriginAirport(

                                      outboundFlight.getOriginAirportId());

      //  Verify  Outcome

      assertOnly1FlightInDtoList(  "Flights  at  origin",

                                                              outboundFlight,

                                                              flightsAtOrigin);

}

public  void  testGetFlightsByFromAirport_TwoOutboundFlights()

                      throws  Exception  {

      setupStandardAirportsAndFlights();

      FlightDto[]  outboundFlights  =

                            findTwoOutboundFlightsFromOneAirport();

      //  Exercise  System

      List  flightsAtOrigin  =  facade.getFlightsByOriginAirport(

                                      outboundFlights[0].getOriginAirportId());

      //  Verify  Outcome

      assertExactly2FlightsInDtoList(  "Flights  at  origin",

                                                                      outboundFlights,

                                                                      flightsAtOrigin);

}

 

毫不奇怪,这些测试很慢,因为创建机场和航班需要数据库。我们可以尝试重构这些测试,在方法中设置 Fixture setUp隐式设置;参见第424页):

Not surprisingly, these tests are slow because creating the airports and flights involves a database. We can try refactoring these tests to set up the fixture in the setUp method (Implicit Setup; see page 424):

protected void setUp() throws Exception {

      façade = new FlightMgmtFacadeImpl();

      helper = new FlightManagementTestHelper();

      setupStandardAirportsAndFlights();

      oneOutboundFlight = findOneOutboundFlight();

}

protected void teaDown() throws Exception {

      removeStandardAirportsAndFlights();

}



public void testGetFlightsByOriginAirport_NoFlights_td()

                throws Exception {

      // Fixture Setup

      BigDecimal outboundAirport = createTestAirport("1OF");

      try {

            // Exercise System

            List flightsAtDestination1 =

                      Façade.getFlightsByOriginAirport(outboundAirport);

            // 验证结果

            assertEquals(0,flightsAtDestination1.size());

      } finally {

              façade.removeAirport(outboundAirport);

      }

}



public void testGetFlightsByFromAirport_OneOutboundFlight()

                      throws Exception {

      // 练习系统

      列表 flightsAtOrigin = Facade.getFlightsByOriginAirport(

                                  oneOutboundFlight.getOriginAirportId());

      // 验证结果

      assertOnly1FlightInDtoList( "Flights at origin",

                                                  oneOutboundFlight,

                                                  flightsAtOrigin);

}



public void testGetFlightsByFromAirport_TwoOutboundFlights()

                      throws Exception {

      FlightDto[] outboundFlights =

                          findTwoOutboundFlightsFromOneAirport();

      // 练习系统

      列表 flightsAtOrigin = Facade.getFlightsByOriginAirport(

                            outboundFlights[0].getOriginAirportId());

      // 验证结果

      assertExactly2FlightsInDtoList( "Flights at origin",

                                                          outboundFlights,

                                                          flightsAtOrigin);

}

protected  void  setUp()  throws  Exception  {

      facade  =  new  FlightMgmtFacadeImpl();

      helper  =  new  FlightManagementTestHelper();

      setupStandardAirportsAndFlights();

      oneOutboundFlight  =  findOneOutboundFlight();

}

protected  void  tearDown()  throws  Exception  {

      removeStandardAirportsAndFlights();

}



public  void  testGetFlightsByOriginAirport_NoFlights_td()

                throws  Exception  {

      //  Fixture  Setup

      BigDecimal  outboundAirport  =  createTestAirport("1OF");

      try  {

            //  Exercise  System

            List  flightsAtDestination1  =

                      facade.getFlightsByOriginAirport(outboundAirport);

            //  Verify  Outcome

            assertEquals(0,flightsAtDestination1.size());

      }  finally  {

              facade.removeAirport(outboundAirport);

      }

}



public  void  testGetFlightsByFromAirport_OneOutboundFlight()

                      throws  Exception  {

      //  Exercise  System

      List  flightsAtOrigin  =  facade.getFlightsByOriginAirport(

                                  oneOutboundFlight.getOriginAirportId());

      //  Verify  Outcome

      assertOnly1FlightInDtoList(  "Flights  at  origin",

                                                  oneOutboundFlight,

                                                  flightsAtOrigin);

}



public  void  testGetFlightsByFromAirport_TwoOutboundFlights()

                      throws  Exception  {

      FlightDto[]  outboundFlights  =

                          findTwoOutboundFlightsFromOneAirport();

      //  Exercise  System

      List  flightsAtOrigin  =  facade.getFlightsByOriginAirport(

                            outboundFlights[0].getOriginAirportId());

      //  Verify  Outcome

      assertExactly2FlightsInDtoList(  "Flights  at  origin",

                                                          outboundFlights,

                                                          flightsAtOrigin);

}

 

这不会加快我们的测试速度,因为测试自动化框架会为每个测试用例对象调用setUp和方法。我们所做的只是移动代码。我们需要找到一种方法,在每次测试运行中只设置一次装置。tearDown

This doesn't speed up our tests one bit because the Test Automation Framework calls the setUp and tearDown methods for each Testcase Object. All we have done is moved the code. We need to find a way to set up the fixture only once per test run.

重构说明

Refactoring Notes

我们可以将此测试转换为Lazy Setup,从而减少设置 Fixture 的次数。由于 Fixture 设置已由该setUp方法处理,因此我们只需将 Lazy Initialization 逻辑插入到该setUp方法中,这样只有第一个测试才会运行它。我们一定不要忘记删除该逻辑,因为如果它在每个测试方法tearDown运行后都删除 Fixture,它将使 Lazy Initialization 逻辑变得毫无用处!抱歉,如果我们的 xUnit 家族成员不支持Suite Fixture Setup,那么我们就没有地方可以移动这个逻辑,以便在最后一个测试方法完成后运行它。

We can reduce the number of times we set up the fixture by converting this test to Lazy Setup. Because the fixture setup is already handled by the setUp method, we need simply insert the Lazy Initialization logic into the setUp method so that only the first test will cause it to be run. We must not forget to remove the tearDown logic, because it will render the Lazy Initialization logic useless if it removes the fixture after each Test Method has run! Sorry, but there is nowhere that we can move this logic to so that it will be run after the last Test Method has completed if our xUnit family member doesn't support Suite Fixture Setup.

示例:惰性设置

Example: Lazy Setup

以下是使用Lazy Setup 重构的相同测试:

Here is the same test refactored to use Lazy Setup:

protected void setUp() throws Exception {

      if (sharedFixtureInitialized) {

            return;

      }

      façade = new FlightMgmtFacadeImpl();

      setupStandardAirportsAndFlights();

      sharedFixtureInitialized = true;

}



protected void teaDown() throws Exception {

      // 无法删除任何对象,因为我们不知道

      // 这是否是最后一次测试

}

protected  void  setUp()  throws  Exception  {

      if  (sharedFixtureInitialized)  {

            return;

      }

      facade  =  new  FlightMgmtFacadeImpl();

      setupStandardAirportsAndFlights();

      sharedFixtureInitialized  =  true;

}



protected  void  tearDown()  throws  Exception  {

      //  Cannot  delete  any  objects  because  we  don't  know

      //  whether  this  is  the  last  test

}

 

tearDown虽然上有方法AirportFixture,但无法知道何时调用它!这是使用Lazy Setup的主要后果。由于变量是静态的,它们不会超出范围;因此,在卸载或重新加载类之前,夹具不会被垃圾收集。

While there is a tearDown method on AirportFixture, there is no way to know when to call it! That's the main consequence of using Lazy Setup. Because the variables are static, they will not go out of scope; hence the fixture will not be garbage collected until the class is unloaded or reloaded.

测试与隐式设置版本相比没有变化:

The tests are unchanged from the Implicit Setup version:

public void testGetFlightsByFromAirport_OneOutboundFlight()

                throws Exception {

      FlightDto outboundFlight = findOneOutboundFlight();

      // 练习系统

      列表 flightsAtOrigin = Facade.getFlightsByOriginAirport(

                                              outboundFlight.getOriginAirportId());

      // 验证结果

      assertOnly1FlightInDtoList( "Flights at origin",

                                                  outboundFlight,

                                                  flightsAtOrigin);

}

public void testGetFlightsByFromAirport_TwoOutboundFlights()

                throws Exception {

      FlightDto[] outboundFlights =

            findTwoOutboundFlightsFromOneAirport();

      // 练习系统

      列表 flightsAtOrigin = facade.getFlightsByOriginAirport(

                                    outboundFlights[0].getOriginAirportId());

      // 验证结果

      assertExactly2FlightsInDtoList( "Flights at origin",

                                                          outboundFlights,

                                                          flightsAtOrigin);

}

public  void  testGetFlightsByFromAirport_OneOutboundFlight()

                throws  Exception  {

      FlightDto  outboundFlight  =  findOneOutboundFlight();

      //  Exercise  System

      List  flightsAtOrigin  =  facade.getFlightsByOriginAirport(

                                              outboundFlight.getOriginAirportId());

      //  Verify  Outcome

      assertOnly1FlightInDtoList(  "Flights  at  origin",

                                                  outboundFlight,

                                                  flightsAtOrigin);

}

public  void  testGetFlightsByFromAirport_TwoOutboundFlights()

                throws  Exception  {

      FlightDto[]  outboundFlights  =

            findTwoOutboundFlightsFromOneAirport();

      //  Exercise  System

      List  flightsAtOrigin  =  facade.getFlightsByOriginAirport(

                                    outboundFlights[0].getOriginAirportId());

      //  Verify  Outcome

      assertExactly2FlightsInDtoList(  "Flights  at  origin",

                                                          outboundFlights,

                                                          flightsAtOrigin);

}

 

套房固定装置设置

Suite Fixture Setup

我们怎样才能在第一个需要它的测试方法之前构建共享夹具?

How do we cause the Shared Fixture to be built before the first test method that needs it?

我们在调用第一个/最后一个测试方法之前/之后,用测试自动化框架调用的特殊方法构建/销毁共享装置。

We build/destroy the shared fixture in special methods called by the Test Automation Framework before/after the first/last Test Method is called.

图像

共享夹具第 317)通常用于减少设置夹具所需的每次测试开销。共享夹具需要额外的测试编程工作,因为我们必须创建夹具,并有办法在每个测试中发现夹具。无论如何访问夹具,都必须在使用它之前对其进行初始化(构造)。

Shared Fixtures (page 317) are commonly used to reduce the amount of per-test overhead required to set up the fixture. Sharing a fixture involves extra test programming effort because we must create the fixture and have a way of discovering the fixture in each test. Regardless of how the fixture is accessed, it must be initialized (constructed) before it is used.

如果所有需要初始化 Fixture的测试方法(第 348页) 都定义在同一个测试用例类(第 373)中,则 Suite Fixture Setup是初始化 Fixture 的一种方法

Suite Fixture Setup is one way to initialize the fixture if all the Test Methods (page 348) that need it are defined on the same Testcase Class (page 373).

工作原理

How It Works

我们实现或重写测试自动化框架(第 298页) 自动调用的一对方法。这些方法的名称或注释在 xUnit 家族的成员之间有所不同,但所有方法的工作方式都相同:框架在调用第一个测试方法的方法之前调用Suite Fixture Setup方法;在调用最后一个测试方法的方法之后调用Suite Fixture Teardown方法。(我更愿意说“第一个/最后一个测试用例对象上的方法”,但事实并非如此:与 xUnit 家族的其他成员不同,NUnit 只创建一个测试用例对象。有关详细信息,请参阅第384页的侧栏“始终存在异常” 。)setUptearDown

We implement or override a pair of methods that the Test Automation Framework (page 298) calls automatically. The name or annotation of these methods varies between members of the xUnit family but all work the same way: The framework calls the Suite Fixture Setup method before it calls the setUp method for the first Test Method; it calls the Suite Fixture Teardown method after it calls the tearDown method for the final Test Method. (I would have preferred to say, "method on the first/final Testcase Object" but that isn't true: NUnit, unlike other members of the xUnit family, creates only a single Testcase Object. See the sidebar "There's Always an Exception" on page 384 for details.)

何时使用它

When to Use It

当我们有一个测试装置,希望在单个测试用例类的所有测试方法之间共享,并且我们的 xUnit 变体支持此功能时,我们可以使用Suite Fixture Setup。如果我们需要在运行最后一个测试后拆除装置,则此模式特别有用。在撰写本书时,只有 VbUnit、NUnit 和 JUnit 4.0 支持“开箱即用的 Suite Fixture Setup ”。尽管如此,在大多数 xUnit 变体中添加此功能并不困难。

We can use Suite Fixture Setup when we have a test fixture we wish to share between all Test Methods of a single Testcase Class and our variant of xUnit supports this feature. This pattern is particularly useful if we need to tear down the fixture after the last test is run. At the time of writing this book, only VbUnit, NUnit, and JUnit 4.0 supported Suite Fixture Setup "out of the box." Nevertheless, it is not difficult to add this capability in most variants of xUnit.

如果我们需要更广泛地共享 Fixture,我们必须使用预构建 Fixture第 429页)、设置装饰器第 447页)或延迟设置(第435页)。如果我们不想共享 Fixture 的实际实例,但确实想共享设置 Fixture 的代码,我们可以使用隐式设置(第424页)或委托设置(第 411页)。

If we need to share the fixture more widely, we must use either a Prebuilt Fixture (page 429), a Setup Decorator (page 447), or Lazy Setup (page 435). If we don't want to share the actual instance of the fixture but we do want to share the code to set up the fixture, we can use Implicit Setup (page 424) or Delegated Setup (page 411).

使用共享夹具(因此称为套件夹具设置)的主要原因是为了克服每次运行每个测试时创建过多测试夹具对象而导致的测试速度慢第 253页)问题。当然,共享夹具可能会导致测试交互(请参阅第 228页的不稳定测试)甚至测试运行战(请参阅不稳定测试);侧栏“不使用共享夹具的更快测试”(第 319页)介绍了解决此问题的其他方法。

The main reason for using a Shared Fixture, and hence Suite Fixture Setup, is to overcome the problem of Slow Tests (page 253) caused by too many test fixture objects being created each time every test is run. Of course, a Shared Fixture can lead to Interacting Tests (see Erratic Test on page 228) or even a Test Run War (see Erratic Test); the sidebar "Faster Tests Without Shared Fixtures" (page 319) describes other ways to solve this problem.

实施说明

Implementation Notes

为了使套件装置设置正常工作,我们必须确保在调用测试方法时记住装置。此标准意味着我们需要使用类变量、注册表[PEAA]或单例[GOF]来保存对装置的引用(NUnit 除外;请参阅第 384页的侧栏“总是有异常” )。确切的实现因 xUnit 系列的不同成员而异。以下是一些重点:

For Suite Fixture Setup to work properly, we must ensure that the fixture is remembered between calls to the Test Methods. This criterion implies we need to use a class variable, Registry [PEAA], or Singleton [GOF] to hold the references to the fixture (except in NUnit; see the sidebar "There's Always an Exception" on page 384). The exact implementation varies from one member of the xUnit family to the next. Here are a few highlights:

  • 在 VbUnit 中,我们在 Testcase 类中实现接口 IFixtureFrame 从而使测试自动化框架(1) 在调用IFixtureFrame_Create第一个测试方法之前调用该方法,以及 (2) 在调用IFixtureFrame_Destroy最后一个测试方法之后调用该方法。
  • In VbUnit, we implement the interface IFixtureFrame in the Testcase Class, thereby causing the Test Automation Framework (1) to call the IFixtureFrame_Create method before the first Test Method is called and (2) to call the IFixtureFrame_Destroy method after the last Test Method is called.
  • 在 NUnit 中,属性[TestFixtureSetUp][TestFixtureTearDown]用于在测试装置内部指定要调用的方法 (1) 在执行装置中的任何测试之前调用一次,以及 (2) 所有测试完成后调用一次。
  • In NUnit, the attributes [TestFixtureSetUp] and [TestFixtureTearDown] are used inside a test fixture to designate the methods to be called (1) once prior to executing any of the tests in the fixture and (2) once after all tests are completed.
  • 在 JUnit 4.0 及更高版本中,该属性@BeforeClass用于指示在执行第一个测试方法之前应运行一次方法。具有该属性的方法@AfterClass在运行最后一个测试方法之后运行。JUnit 允许继承和覆盖这些方法;子类的方法在超类的方法之间运行。
  • In JUnit 4.0 and later, the attribute @BeforeClass is used to indicate that a method should be run once before the first Test Method is executed. The method with the attribute @AfterClass is run after the last Test Method is run. JUnit allows these methods to be inherited and overridden; the subclass's methods are run between the superclass's methods.

仅仅因为我们使用Implicit Setup的形式来调用测试装置的构造和析构,并不意味着我们应该将所有装置设置逻辑都转储到Suite Fixture Setup中。我们可以从Suite Fixture Setup方法中调用Creation Methods第 415页),将复杂的构造逻辑移到更易于测试和重用的地方,例如Testcase Superclass第 638页)或Test Helper第 643页)。

Just because we use a form of Implicit Setup to invoke the construction and destruction of the test fixture, it doesn't mean that we should dump all the fixture setup logic into the Suite Fixture Setup. We can call Creation Methods (page 415) from the Suite Fixture Setup method to move complex construction logic into places where it can be tested and reused more easily, such as a Testcase Superclass (page 638) or a Test Helper (page 643).

激励人心的例子

Motivating Example

假设我们有以下测试:

Suppose we have the following test:

[设置]

protected void setUp() {

      helper.setupStandardAirportsAndFlights();

}



[拆卸]

protected void teaDown() {

      helper.removeStandardAirportsAndFlights();

}



[测试]

public void testGetFlightsByOriginAirport_2OutboundFlights(){

      FlightDto[] expectedFlights =

                helper.findTwoOutboundFlightsFromOneAirport();

      long originAirportId = expectedFlights[0].OriginAirportId;

      // 练习系统

      IList flightsAtOrigin =

                Facade.GetFlightsByOriginAirport(originAirportId);

      // 验证结果

      AssertExactly2FlightsInDtoList(

                  expectedFlights[0], expectedFlights[1],

                  flightsAtOrigin, "Flights at origin");

}



[测试]

public void testGetFlightsByOriginAirport_OneOutboundFlight(){

      FlightDto expectedFlight = helper.findOneOutboundFlight();

      // 练习系统

      IList flightsAtOrigin = Facade.GetFlightsByOriginAirport(

            expectedFlight.OriginAirportId);

      // 验证结果

      AssertOnly1FlightInDtoList( expectedFlight,

                          flightsAtOrigin, "出发航班在原产地");

}

[SetUp]

protected  void  setUp()  {

      helper.setupStandardAirportsAndFlights();

}



[TearDown]

protected  void  tearDown()    {

      helper.removeStandardAirportsAndFlights();

}



[Test]

public  void  testGetFlightsByOriginAirport_2OutboundFlights(){

      FlightDto[]  expectedFlights  =

                helper.findTwoOutboundFlightsFromOneAirport();

      long  originAirportId  =  expectedFlights[0].OriginAirportId;

      //  Exercise  System

      IList  flightsAtOrigin  =

                facade.GetFlightsByOriginAirport(originAirportId);

      //  Verify  Outcome

      AssertExactly2FlightsInDtoList(

                  expectedFlights[0],  expectedFlights[1],

                  flightsAtOrigin,      "Flights  at  origin");

}



[Test]

public  void  testGetFlightsByOriginAirport_OneOutboundFlight(){

      FlightDto  expectedFlight  =  helper.findOneOutboundFlight();

      //  Exercise  System

      IList  flightsAtOrigin  =  facade.GetFlightsByOriginAirport(

            expectedFlight.OriginAirportId);

      //  Verify  Outcome

      AssertOnly1FlightInDtoList(  expectedFlight,

                          flightsAtOrigin,  "Outbound  flight  at  origin");

}

 

图 20.1是这些测试的仪器版本生成的控制台。

Figure 20.1 is the console generated by an instrumented version of these tests.

图 20.1。 隐式设置和测试方法的调用顺序。该 setupStandardAirportsAndFlights 方法在每个测试方法之前调用。水平线划定了测试方法的边界。

Figure 20.1. The calling sequence of Implicit Setup and Test Methods. The setupStandardAirportsAndFlights method is called before each Test Method. The horizontal lines delineate the Test Method boundaries.

--------------------

  设置

    setupStandardAirportsAndFlights

  testGetFlightsByOriginAirport_OneOutboundFlight

  拆除

    删除StandardAirportsAndFlights

--------------------

  设置

    setupStandardAirportsAndFlights

  testGetFlightsByOriginAirport_TwoOutboundFlights

  拆除

    删除StandardAirportsAndFlights

--------------------

--------------------

  setUp

    setupStandardAirportsAndFlights

  testGetFlightsByOriginAirport_OneOutboundFlight

  tearDown

    removeStandardAirportsAndFlights

--------------------

  setUp

    setupStandardAirportsAndFlights

  testGetFlightsByOriginAirport_TwoOutboundFlights

  tearDown

    removeStandardAirportsAndFlights

--------------------

 

重构说明

Refactoring Notes

假设我们想将此示例重构为Shared Fixture。如果我们不关心测试运行结束时销毁 Fixture,我们可以使用Lazy Setup。否则,我们可以将此示例转换为 Suite Fixture Setup 策略,只需将代码分别从setUptearDown方法移动到suiteFixtureSetUpsuiteFixtureTearDown方法即可。

Suppose we want to refactor this example to a Shared Fixture. If we don't care about destroying the fixture when the test run is finished, we could use Lazy Setup. Otherwise, we can convert this example to a Suite Fixture Setup strategy by simply moving our code from the setUp and tearDown methods to the suiteFixtureSetUp and suiteFixtureTearDown methods, respectively.

在 NUnit 中,我们使用属性[TestFixtureSetUp]和向测试自动化框架[TestFixtureTearDown]指示这些方法。如果我们不想在方法中留下任何东西,我们可以简单地将属性从和分别更改为和。setUp/tearDown[Setup]TearDown[TestFixtureSetUp][TestFixtureTearDown]

In NUnit, we use the attributes [TestFixtureSetUp] and [TestFixtureTearDown] to indicate these methods to the Test Automation Framework. If we don't want to leave anything in our setUp/tearDown methods, we can simply change the attributes from [Setup] and TearDown to [TestFixtureSetUp] and [TestFixtureTearDown], respectively.

示例:套件装置设置

Example: Suite Fixture Setup

以下是我们对Suite Fixture Setup进行重构的结果:

Here's the result of our refactoring to Suite Fixture Setup:

[TestFixtureSetUp]

protected void suiteFixtureSetUp()

{

      helper.setupStandardAirportsAndFlights();

}



[TestFixtureTearDown]

protected void suiteFixtureTearDown() {

      helper.removeStandardAirportsAndFlights();

}



[SetUp]

protected void setUp() {

}



[TearDown]

protected void teaDown() {

}



[Test]

public void testGetFlightsByOrigin_TwoOutboundFlights(){

      FlightDto[] expectedFlights =

                helper.findTwoOutboundFlightsFromOneAirport();

      long originAirportId = expectedFlights[0].OriginAirportId;

      // 练习系统

      IList flightsAtOrigin =

                Facade.GetFlightsByOriginAirport(originAirportId);

      // 验证结果

      AssertExactly2FlightsInDtoList(

                expectedFlights[0], expectedFlights[1],

                flightsAtOrigin, "出发地航班");

}



[测试]

public void testGetFlightsByOrigin_OneOutboundFlight() {

      FlightDto expectedFlight = helper.findOneOutboundFlight();

      // 练习系统

      IList flightsAtOrigin = Facade.GetFlightsByOriginAirport(

            expectedFlight.OriginAirportId);

      // 验证结果

      AssertOnly1FlightInDtoList( expectedFlight,

                          flightsAtOrigin, "出发地出站航班");

}

[TestFixtureSetUp]

protected  void  suiteFixtureSetUp()

{

      helper.setupStandardAirportsAndFlights();

}



[TestFixtureTearDown]

protected  void  suiteFixtureTearDown()    {

      helper.removeStandardAirportsAndFlights();

}



[SetUp]

protected  void  setUp()  {

}



[TearDown]

protected  void  tearDown()    {

}



[Test]

public  void  testGetFlightsByOrigin_TwoOutboundFlights(){

      FlightDto[]  expectedFlights  =

                helper.findTwoOutboundFlightsFromOneAirport();

      long  originAirportId  =  expectedFlights[0].OriginAirportId;

      //  Exercise  System

      IList  flightsAtOrigin  =

                facade.GetFlightsByOriginAirport(originAirportId);

      //  Verify  Outcome

      AssertExactly2FlightsInDtoList(

                expectedFlights[0],  expectedFlights[1],

                flightsAtOrigin,        "Flights  at  origin");

}



[Test]

public  void  testGetFlightsByOrigin_OneOutboundFlight()  {

      FlightDto  expectedFlight  =  helper.findOneOutboundFlight();

      //  Exercise  System

      IList  flightsAtOrigin  =  facade.GetFlightsByOriginAirport(

            expectedFlight.OriginAirportId);

      //  Verify  Outcome

      AssertOnly1FlightInDtoList(  expectedFlight,

                          flightsAtOrigin,  "Outbound  flight  at  origin");

}

 

现在,当调用Testcase 类的各种方法时,控制台看起来就像图 20.2一样。

Now when various methods of the Testcase Class are called, the console looks like Figure 20.2.

图 20.2。套件装置设置和测试方法的调用顺序。setupStandardAndAirportsAndFlights方法仅针对测试用例类调用一次,而不是在每个测试方法之前调用。水平线划定了测试方法的边界。

Figure 20.2. The calling sequence of Suite Fixture Setup and Test Methods. The setupStandardAndAirportsAndFlights method is called once only for the Testcase Class rather than before each Test Method. The horizontal lines delineate the Test Method boundaries.

suiteFixtureSetUp

        设置StandardAirportsAndFlights

    --------------------

      设置

      testGetFlightsByOriginAirport_OneOutboundFlight

      拆卸

    --------------------

      设置

      testGetFlightsByOriginAirport_TwoOutboundFlights

      拆卸

    --------------------

suiteFixtureTearDown

        删除StandardAirportsAndFlights

suiteFixtureSetUp

        setupStandardAirportsAndFlights

    --------------------

      setUp

      testGetFlightsByOriginAirport_OneOutboundFlight

      tearDown

    --------------------

      setUp

      testGetFlightsByOriginAirport_TwoOutboundFlights

      tearDown

    --------------------

suiteFixtureTearDown

        removeStandardAirportsAndFlights

 

setUp方法仍然在每个测试方法之前调用,以及suiteFixtureSetUp我们现在调用setupStandardAirportsAndFlights来设置我们的装置的方法。到目前为止,这与Lazy Setup没有什么不同;不同之处在于它removeStandardAirportsAndFlights是在我们的最后一个测试方法之后调用的。

The setUp method is still called before each Test Method, along with the suiteFixtureSetUp method where we are now calling setupStandardAirportsAndFlights to set up our fixture. So far, this is no different than Lazy Setup; the difference arises in that removeStandardAirportsAndFlights is called after the last of our Test Methods.

关于名称

About the Name

命名这个模式很困难,因为实现它的 xUnit 的每个变体都有不同的名称。使问题复杂化的是,微软阵营使用的“测试装置”的含义比 Java/Pearl/Ruby/等阵营的含义要多。我通过关注共享装置的范围,找到了套件装置设置;它在整个测试套件中共享,一个测试用例类会生成一个测试套件对象第 387页)。为测试套件对象构建的装置可以称为“SuiteFixture”。

Naming this pattern was tough because each variant of xUnit that implements it has a different name for it. Complicating matters is the fact that the Microsoft camp uses "test fixture" to mean more than what the Java/Pearl/Ruby/etc. camp means. I landed on Suite Fixture Setup by focusing on the scope of the Shared Fixture; it is shared across the test suite for one Testcase Class that spawns a single Test Suite Object (page 387). The fixture that is built for the Test Suite Object could be called a "SuiteFixture."

进一步阅读

有关VbUnit 中实现的Suite Fixture Setup的更多信息,请参阅http://www.vbunit.com/doc/Advanced.htm 。有关NUnit 中实现的Suite Fixture Setup的更多信息,请参阅http://nunit.org 。

See http://www.vbunit.com/doc/Advanced.htm for more information on Suite Fixture Setup as implemented in VbUnit. See http://nunit.org for more information on Suite Fixture Setup as implemented in NUnit.

安装装饰器

Setup Decorator

我们怎样才能在第一个需要它的测试方法之前构建共享夹具?

How do we cause the Shared Fixture to be built before the first test method that needs it?

我们用装饰器包装测试套件,它在运行测试之前设置共享的测试装置,并在所有测试完成后将其拆除。

We wrap the test suite with a Decorator that sets up the shared test fixture before running the tests and tears it down after all tests are done.

图像

如果我们选择使用共享夹具第 317页),无论是出于方便还是必要,并且我们选择不使用预构建夹具(第429页),我们将需要确保在每次测试运行之前构建夹具。延迟设置第 435页)是我们可以采用的一种策略,用于为第一个测试“及时”创建测试夹具。但如果在最后一个测试后拆除夹具至关重要,我们如何知道所有测试都已完成?

If we have chosen to use a Shared Fixture (page 317), whether for reasons of convenience or out of necessity, and we have chosen not to use a Prebuilt Fixture (page 429), we will need to ensure that the fixture is built before each test run. Lazy Setup (page 435) is one strategy we could employ to create the test fixture "just in time" for the first test. But if it is critical to tear down the fixture after the last test, how do we know that all tests have been completed?

工作原理

How It Works

安装装饰器的工作方式是使用一组匹配setUptearDown“书挡”来“包围”整个测试套件的执行。模式装饰器[GOF]正是我们实现此目的所需要的。我们构造一个安装装饰器,它保存对我们希望装饰的测试套件对象(第 387页) 的引用,然后将我们的装饰器传递给测试运行器(第 377页)。当需要运行测试时,测试运行器会调用安装装饰器run上的方法,而不是实际测试套件对象上的方法。安装装饰器在调用测试套件对象上的方法之前执行装置设置,并在返回后拆除装置。runrun

A Setup Decorator works by "bracketing" the execution of the entire test suite with a set of matching setUp and tearDown "bookends." The pattern Decorator [GOF] is just what we need to make this happen. We construct a Setup Decorator that holds a reference to the Test Suite Object (page 387) we wish to decorate and then pass our Decorator to the Test Runner (page 377). When it is time to run the test, the Test Runner calls the run method on our Setup Decorator rather than the run method on the actual Test Suite Object. The Setup Decorator performs the fixture setup before calling the run method on the Test Suite Object and tears down the fixture after it returns.

何时使用它

When to Use It

如果在每次测试运行前设置共享装置,并在运行完成后拆除装置,这一点至关重要,则可以使用设置装饰器。此行为可能至关重要,因为测试使用的是硬编码值(请参阅第 714页的文字值),如果每次运行后没有清理,就再次运行测试,会导致测试失败(不可重复的测试;请参阅第 228页的不稳定测试)。或者,此行为可能是必要的,以避免某些有限资源的增量消耗,例如我们的数据库会因重复测试运行而慢慢填满数据。

We can use a Setup Decorator when it is critical that a Shared Fixture be set up before every test run and that the fixture is torn down after the run is complete. This behavior may be critical because tests are using Hard-Coded Values (see Literal Value on page 714) that would cause the tests to fail if they are run again without cleaning up after each run (Unrepeatable Tests; see Erratic Test on page 228). Alternatively, this behavior may be necessary to avoid the incremental consumption of some limited resource, such as our database slowly filling up with data from repeated test runs.

当测试需要在执行 SUT 之前更改某些全局参数,然后在测试完成后需要将此参数改回原样时,我们也可以使用安装装饰器。为了避免测试速度变慢(第253页),用虚假数据库(请参阅第 551页的虚假对象)替换数据库是采用这种方法的一个常见原因;将全局开关设置为特定配置是另一个原因。安装装饰器是在运行时安装的,因此没有什么可以阻止我们在不同的时间(甚至同时)对同一测试套件使用多个不同的装饰器。

We might also use a Setup Decorator when the tests need to change some global parameter before exercising the SUT and then need to change this parameter back when they are finished. Replacing the database with a Fake Database (see Fake Object on page 551) in an effort to avoid Slow Tests (page 253) is one common reason for taking this approach; setting global switches to a particular configuration is another. Setup Decorators are installed at runtime, so nothing stops us from using several different decorators on the same test suite at different times (or even the same time).

作为 Setup Decorator 的替代方案如果我们只想在单个Testcase Class第 373页)中的测试之间共享 Fixture,并且我们的 xUnit 系列成员支持此行为,我们可以使用Suite Fixture Setup第 441页) 。如果不必在每次测试运行后拆除 Fixture,我们可以改用Lazy Setup

As an alternative to a Setup Decorator, we can use Suite Fixture Setup (page 441) if we only want to share the fixture across the tests in a single Testcase Class (page 373) and our member of the xUnit family supports this behavior. If it is not essential that the fixture be torn down after every test run, we could use Lazy Setup instead.

实施说明

Implementation Notes

安装装饰器由一个对象组成,该对象设置装置、将测试执行委托给要运行的测试套件,然后执行代码以拆除装置。为了更好地符合正常的 xUnit 调用约定,我们通常将构建测试装置的代码放入名为 的方法中setUp,将拆除装置的代码放入名为 的方法中tearDown。然后我们的安装装饰器的 run逻辑由三行代码组成:

A Setup Decorator consists of an object that sets up the fixture, delegates test execution to the test suite to be run, and then executes the code to tear down the fixture. To better line up with the normal xUnit calling conventions, we typically put the code that constructs the test fixture into a method called setUp and the code that tears down the fixture into a method called tearDown. Then our Setup Decorator's run logic consists of three lines of code:

void run() {

      设置();

      decoratedSuite.run();

      拆卸();

}

void  run()  {

      setup();

      decoratedSuite.run();

      teardown();

}

 

有多种方法可以构建安装装饰器

There are several ways to build the Setup Decorator.

变体:抽象设置装饰器

测试自动化框架xUnit 系列(第 298页) 的许多成员都提供了一个可重用的超类,该超类实现了Setup Decorator。此类通常将setUp/run/tearDown序列实现为模板方法[GOF]。我们所要做的就是将此类子类化,并像在普通测试用例类setUp中一样实现和方法。在实例化我们的Setup Decorator类时,我们将要装饰的测试套件对象作为构造函数参数传递。tearDown

Many members of the xUnit family of Test Automation Frameworks (page 298) provide a reusable superclass that implements a Setup Decorator. This class usually implements the setUp/run/tearDown sequence as a Template Method [GOF]. All we have to do is to subclass this class and implement the setUp and tearDown methods as we would in a normal Testcase Class. When instantiating our Setup Decorator class, we pass the Test Suite Object we are decorating as the constructor argument.

变体:硬编码安装装饰器

如果我们需要从头开始构建我们的安装装饰器,那么“最简单的方法”就是在安装装饰器suite的方法中硬编码装饰类的名称。这样安装装饰器类就可以充当装饰套件的测试套件工厂(请参阅第399页的测试枚举)。

If we need to build our Setup Decorator from scratch, the "simplest thing that could possibly work" is to hard-code the name of the decorated class in the suite method of the Setup Decorator. This allows the Setup Decorator class to act as the Test Suite Factory (see Test Enumeration on page 399) for the decorated suite.

变体:参数化设置装饰器

如果我们想为不同的测试套件重用Setup Decorator ,我们可以用要运行的测试套件对象参数化其构造函数方法。这意味着可以在Setup Decorator 中编写设置和拆卸逻辑,从而无需使用单独的测试助手(第643页) 类来跨测试重用设置逻辑。

If we want to reuse the Setup Decorator for different test suites, we can parameterize its constructor method with the Test Suite Object to be run. This means that the setup and teardown logic can be coded within the Setup Decorator, thereby eliminating the need for a separate Test Helper (page 643) class just to reuse the setup logic across tests.

变体:装饰性懒人设置

使用安装装饰器的主要缺点之一是测试无法自行运行,因为它们依赖于安装装饰器来设置装置。我们可以通过在方法中使用延迟安装来增强安装装饰器来解决此要求,以便未装饰的测试用例对象(第 382页) 可以构建自己的装置。测试用例对象还可以记住它已构建自己的装置并在方法中销毁它。此功能可以在通用测试用例超类(第 638页) 上实现,这样只需构建和测试一次即可。setUptearDown

One of the main drawbacks of using a Setup Decorator is that tests cannot be run by themselves because they depend on the Setup Decorator to set up the fixture. We can work around this requirement by augmenting the Setup Decorator with Lazy Setup in the setUp method so that an undecorated Testcase Object (page 382) can construct its own fixture. The Testcase Object can also remember that it built its own fixture and destroy it in the tearDown method. This functionality could be implemented on a generic Testcase Superclass (page 638) so that it has to be built and tested just once.

唯一的其他选择是使用Pushdown Decorator。但是,这会抵消Shared Fixture给我们带来的任何测试加速,因此这种方法只能在我们出于设置Shared Fixture以外的原因使用Setup Decorator 的情况下使用。

The only other alternative is to use a Pushdown Decorator. That would negate any test speedup the Shared Fixture bought us, however, so this approach can be used only in those cases when we use the Setup Decorator for reasons other than setting up a Shared Fixture.

变体:下推式装饰器

使用安装装饰器的主要缺点之一是测试不能自行运行,因为它们依赖于安装装饰器来设置装置。 我们可以绕过这个障碍的一种方法是提供一种方法将装饰器下推到单个测试的级别,而不是整个测试套件的级别。 此步骤需要对类进行一些修改,TestSuite以允许将安装装饰器传递到在测试发现(第393页) 过程中构造单个测试用例对象的地方。 由于每个对象都是从测试方法(第 348页)创建的,因此在将其添加到测试套件对象的测试集合之前,它会被包装在安装装饰器中。

One of the main drawbacks of using a Setup Decorator is that tests cannot be run by themselves because they depend on the Setup Decorator to set up the fixture. One way we can circumvent this obstacle is to provide a means to push the decorator down to the level of the individual tests rather than the whole test suite. This step requires a few modifications to the TestSuite class to allow the Setup Decorator to be passed down to where the individual Testcase Objects are constructed during the Test Discovery (page 393) process. As each object is created from the Test Method (page 348), it is wrapped in the Setup Decorator before it is added to the Test Suite Object's collection of tests.

当然,这会抵消使用Setup Decorator所带来的速度优势的主要来源之一,因为这样会强制为每个测试构建一个新的测试装置。请参阅第 319页的侧栏“不使用共享装置进行更快的测试”,了解解决测试执行速度问题的其他方法。

Of course, this negates one of the major sources of the speed advantage created by using a Setup Decorator by forcing a new test fixture to be built for each test. See the sidebar "Faster Tests Without Shared Fixtures" on page 319 for other ways to address the test execution speed issue.

激励人心的例子

Motivating Example

在此示例中,我们有一组测试使用延迟设置来构建共享装置Finder 方法(请参阅第599页的测试实用程序方法)来查找装置中的对象。我们发现剩余的装置导致了不可重复的测试,因此我们希望在最后一个测试运行完毕后进行适当的清理。

In this example, we have a set of tests that use Lazy Setup to build the Shared Fixture and Finder Methods (see Test Utility Method on page 599) to find the objects in the fixture. We have discovered that the leftover fixture is causing Unrepeatable Tests, so we want to clean up properly after the last test has finished running.

protected void setUp() throws Exception {

      if (sharedFixtureInitialized) {

            return;

      }

      façade = new FlightMgmtFacadeImpl();

      setupStandardAirportsAndFlights();

      sharedFixtureInitialized = true;

}



protected void teaDown() throws Exception {

      // 无法删除任何对象,因为我们不知道

      // 这是否是最后一次测试

}

protected  void  setUp()  throws  Exception  {

      if  (sharedFixtureInitialized)  {

            return;

      }

      facade  =  new  FlightMgmtFacadeImpl();

      setupStandardAirportsAndFlights();

      sharedFixtureInitialized  =  true;

}



protected  void  tearDown()  throws  Exception  {

      //  Cannot  delete  any  objects  because  we  don't  know

      //  whether  this  is  the  last  test

}

 

因为没有简单的方法可以通过延迟安装来实现这个目标,所以我们必须改变我们的装置安装策略。一个选择是改用安装装饰器

Because there is no easy way to accomplish this goal with Lazy Setup, we must change our fixture setup strategy. One option is to use a Setup Decorator instead.

重构说明

Refactoring Notes

创建Setup Decorator时,我们可以重复使用完全相同的装置设置逻辑;我们只需要在不同的时间调用它。因此,此重构主要包括将对装置设置逻辑的调用从Testcase ClasssetUp上的方法移至Setup Decorator类的方法。假设我们有一个可用于子类的抽象 Setup Decorator ,我们可以创建新的子类并提供和方法的具体实现。setUpsetUptearDown

When creating a Setup Decorator, we can reuse the exact same fixture setup logic; we just need to call it at a different time. Thus this refactoring consists mostly of moving the call to the fixture setup logic from the setUp method on the Testcase Class to the setUp method of a Setup Decorator class. Assuming we have an Abstract Setup Decorator available to subclass, we can create our new subclass and provide concrete implementations of the setUp and tearDown methods.

如果我们的 xUnit 实例不直接支持Setup Decorator,我们可以创建自己的Setup Decorator超类,方法是构建一个单一用途的Setup Decorator,然后引入构造函数参数和实例变量来保存要运行的测试套件。最后,我们执行 Extract Superclass [Fowler] 重构以创建可重用的超类。

If our instance of xUnit does not support Setup Decorator directly, we can create our own Setup Decorator superclass by building a single-purpose Setup Decorator and then introducing a constructor parameter and instance variable to hold the test suite to be run. Finally, we do an Extract Superclass [Fowler] refactoring to create our reusable superclass.

示例:硬编码设置装饰器

Example: Hard-Coded Setup Decorator

在这个例子中,我们将所有设置逻辑移到了安装装饰器setUp的方法中,该方法从抽象安装装饰器继承了其基本功能。我们还在方法中编写了一些夹具拆卸逻辑,以便在运行整个测试套件后清理夹具。tearDown

In this example, we have moved all of the setup logic to the setUp method of a Setup Decorator that inherits its basic functionality from an Abstract Setup Decorator. We have also written some fixture teardown logic in the tearDown method so that we clean up the fixture after the entire test suite has been run.

public class FlightManagementTestSetup extends TestSetup {

      private FlightManagementTestHelper helper;



      public FlightManagementTestSetup() {

            // 构造要装饰的测试套件对象并

            // 将其传递给我们的抽象设置装饰器超类

            super( SafeFlightManagementFacadeTest.suite() );

            helper = new FlightManagementTestHelper();

      }



      public void setUp() throws Exception {

            helper.setupStandardAirportsAndFlights();

      }



      public void teaDown() throws Exception {

            helper.removeStandardAirportsAndFlights();

      }



      public static Test suite() {

            // 返回此装饰器类的一个实例

            return new FlightManagementTestSetup();

      }

}

public  class  FlightManagementTestSetup  extends  TestSetup  {

      private  FlightManagementTestHelper  helper;



      public  FlightManagementTestSetup()  {

            //  Construct  the  Test  Suite  Object  to  be  decorated  and

            //  pass  it  to  our  Abstract  Setup  Decorator  superclass

            super(  SafeFlightManagementFacadeTest.suite()  );

            helper  =  new  FlightManagementTestHelper();

      }



      public  void  setUp()  throws  Exception  {

            helper.setupStandardAirportsAndFlights();

      }



      public  void  tearDown()  throws  Exception  {

            helper.removeStandardAirportsAndFlights();

      }



      public  static  Test  suite()  {

            //  Return  an  instance  of  this  decorator  class

            return  new  FlightManagementTestSetup();

      }

}

 

因为这是一个硬编码的设置装饰器,所以对构建实际测试套件对象的测试套件工厂的调用是硬编码在构造函数内部的。该方法只是调用构造函数。suite

Because this is a Hard-Coded Setup Decorator, the call to the Test Suite Factory that builds the actual Test Suite Object is hard-coded inside the constructor. The suite method just calls the constructor.

示例:参数化设置装饰器

Example: Parameterized Setup Decorator

为了使我们的安装装饰器可以在几个不同的测试套件中重复使用,我们需要在构造函数中对测试套件工厂的名称进行引入参数[JBrains]重构:

To make our Setup Decorator reusable with several different test suites, we need to do an Introduce Parameter [JBrains] refactoring on the name of the Test Suite Factory inside the constructor:

公共类 ParameterizedFlightManagementTestSetup 扩展了 TestSetup {



      私有 FlightManagementTestHelper 助手 =

                新 FlightManagementTestHelper();



      公共 ParameterizedFlightManagementTestSetup(

                                                  测试 testSuiteToDecorate) {

          超级(testSuiteToDecorate);

      }



      公共 void setUp() 抛出异常 {

            助手.setupStandardAirportsAndFlights();

      }



      公共 void teaDown() 抛出异常 {

            助手.removeStandardAirportsAndFlights();

      }

}

public  class  ParameterizedFlightManagementTestSetup  extends  TestSetup  {



      private  FlightManagementTestHelper  helper  =

                new  FlightManagementTestHelper();



      public  ParameterizedFlightManagementTestSetup(

                                                  Test  testSuiteToDecorate)  {

          super(testSuiteToDecorate);

      }



      public  void  setUp()  throws  Exception  {

            helper.setupStandardAirportsAndFlights();

      }



      public  void  tearDown()  throws  Exception  {

            helper.removeStandardAirportsAndFlights();

      }

}

 

为了让测试运行器轻松创建我们的测试套件,我们还需要创建一个测试套件工厂,该工厂使用要装饰的测试套件对象调用设置装饰器的构造函数:

To make it easy for the Test Runner to create our test suite, we also need to create a Test Suite Factory that calls the Setup Decorator's constructor with the Test Suite Object to be decorated:

public class DecoratedFlightManagementFacadeTestFactory {

      public static Test suite() {

            // 返回一个适当装饰的新测试套件对象

            return new ParameterizedFlightManagementTestSetup(

                                 SafeFlightManagementFacadeTest.suite());

      }

}

public  class  DecoratedFlightManagementFacadeTestFactory  {

      public  static  Test  suite()  {

            //  Return  a  new  Test  Suite  Object  suitably  decorated

            return  new  ParameterizedFlightManagementTestSetup(

                                 SafeFlightManagementFacadeTest.suite());

      }

}

 

对于我们希望能够独立运行的每个测试套件,我们都需要其中一个测试套件工厂。即便如此,这也是重用实际的Setup Decorator所付出的一点小代价。

We will need one of these Test Suite Factories for each test suite we want to be able to run by itself. Even so, this is a small price to pay for reusing the actual Setup Decorator.

示例:抽象装饰器类

Example: Abstract Decorator Class

抽象装饰器类如下所示:

Here's what the Abstract Decorator Class looks like:

public class TestSetup extends TestCase {

      Test decoratedSuite;



      AbstractSetupDecorator(Test testSuiteToDecorate) {

            decoratedSuite = testSuiteToDecorate;

      }



      public void setUp() throws Exception {

            // 子类责任

      }



      public void teaDown() throws Exception {

            // 子类责任

      }



      void run() {

            setup();

            decoratedSuite.run();

            teadown();

      }

}

public  class  TestSetup  extends  TestCase  {

      Test  decoratedSuite;



      AbstractSetupDecorator(Test  testSuiteToDecorate)  {

            decoratedSuite  =  testSuiteToDecorate;

      }



      public  void  setUp()  throws  Exception  {

            //  subclass  responsibility

      }



      public  void  tearDown()  throws  Exception  {

            //  subclass  responsibility

      }



      void  run()  {

            setup();

            decoratedSuite.run();

            teardown();

      }

}

 

链式测试

Chained Tests

我们怎样才能在第一个需要它的测试方法之前构建共享夹具?

How do we cause the Shared Fixture to be built before the first test method that needs it?

我们让测试套件中的其他测试设置测试装置。

We let the other tests in a test suite set up the test fixture.

图像

共享夹具第 317)通常用于减少设置夹具所需的每次测试开销。共享夹具需要额外的测试编程工作,因为我们需要创建夹具,并有办法在每个测试中发现夹具。无论如何访问夹具,都必须在使用它之前对其进行初始化(构造)。

Shared Fixtures (page 317) are commonly used to reduce the amount of per-test overhead required to set up the fixture. Sharing a fixture involves extra test programming effort because we need to create the fixture and have a way of discovering the fixture in each test. Regardless of how the fixture is accessed, it must be initialized (constructed) before it is used.

链式测试提供了一种方法来重用一个测试剩下的测试装置以及后续测试的共享装置。

Chained Tests offer a way to reuse the test fixture left over from one test and the Shared Fixture of a subsequent test.

工作原理

How It Works

链式测试利用测试套件中当前测试之前运行的测试所创建的对象。这种方法与人工测试人员在单个测试中测试大量测试条件的方式非常相似 - 通过一系列操作构建复杂的测试装置,并首先验证每个操作的结果。我们可以通过构建一组自检测试(参见第26)来实现自动化测试的类似结果,这些测试不执行任何装置设置,而是依赖于在它们之前运行的测试的“剩余部分”。与装置设置的重用测试模式(参见415页的创建方法)不同,我们实际上并没有从测试中调用另一个测试方法第 348假设它已经运行并留下了一些我们可以用作测试装置的东西。

Chained Tests take advantage of the objects created by the tests that run before our current test in the test suite. This approach is very similar to how a human tester tests a large number of test conditions in a single test—by building up a complex test fixture through a series of actions, with the outcome of each action first being verified. We can achieve a similar result with automated tests by building a set of Self-Checking Tests (see page 26) that do not perform any fixture setup but instead rely on the "leftovers" of the test(s) that run before them. Unlike with the Reuse Test for Fixture Setup pattern (see Creation Method on page 415), we don't actually call another Test Method (page 348) from within out test; we just assume that it has been run and has left behind something we can use as a test fixture.

何时使用它

When to Use It

链式测试是一种人们对其有爱有恨的夹具策略。讨厌它的人之所以讨厌它,是因为这种方法只不过是将测试异味交互测试(请参阅第228页的“不稳定测试”)重塑为一种模式。喜欢它的人通常是因为它解决了使用共享夹具处理慢速测试第 253)所引入的棘手问题。无论哪种方式,它都是重构过长且包含许多相互构建的步骤的现有测试的有效策略。当第一个断言失败时,此类测试将停止执行。我们可以相当快地将此类测试重构为一组链式测试,因为此策略不需要确定我们需要为每个测试构建哪个测试夹具。这可能是将测试演变为一组独立测试的第一步(请参阅第42)。

Chained Tests is a fixture strategy that people either love or hate. Those who hate it do so because this approach is simply the test smell Interacting Tests (see Erratic Test on page 228) recast as a pattern. Those who love it typically do so because it solves a nasty problem introduced by using Shared Fixtures to deal with Slow Tests (page 253). Either way, it is a valid strategy for refactoring existing tests that are overly long and contain many steps that build on one another. Such tests will stop executing when the first assertion fails. We can refactor such tests into a set of Chained Tests fairly quickly because this strategy doesn't require determining exactly which test fixture we need to build for each test. This may be the first step in evolving the tests into a set of Independent Tests (see page 42).

链式测试有助于防止易碎测试第 239SUT API 封装的一种粗糙形式(请参阅599页的测试实用程序方法)。我们的测试不需要与 SUT 交互来设置装置,因为我们让另一个已经使用相同 API 的测试为我们设置了装置。易碎装置(请参阅易碎测试)可能是一个问题;如果修改前述测​​试之一以创建不同的装置,则相关测试可能会失败。如果某些先前的测试失败或出现错误,也是如此;它们可能会使共享装置处于与当前测试预期不同的状态。

Chained Tests help prevent Fragile Tests (page 239) because they are a crude form of SUT API Encapsulation (see Test Utility Method on page 599). Our test doesn't need to interact with the SUT to set up the fixture because we let another test that was already using the same API set up the fixture for us. Fragile Fixtures (see Fragile Test) may be a problem, however; if one of the preceding tests is modified to create a different fixture, the depending test will probably fail. This is also true if some of the earlier tests fail or have errors; they may leave the Shared Fixture in a different state from what the current test expects.

链式测试的一个关键问题是 xUnit 在测试套件中执行测试的顺序不确定。该系列的大多数成员都不保证此顺序(TestNG 是个例外)。因此,当安装新版本的 xUnit 时,甚至当其中一个测试方法重命名时,测试可能会开始失败[如果 xUnit 实现恰好按方法名称对测试用例对象(第 382页) 进行排序]。

One of the key problems with Chained Tests is the nondeterminism of the order in which xUnit executes tests in a test suite. Most members of the family make no guarantees about this order (TestNG is an exception). Thus tests could start to fail when a new version of xUnit is installed or even when one of the Test Methods is renamed [if the xUnit implementation happens to sort the Testcase Objects (page 382) by method name].

另一个问题是,链式测试孤独的测试(参见不稳定测试),因为当前测试依赖于它之前的测试来设置测试装置。如果我们单独运行测试,它很可能会失败,因为它假定的测试装置没有为它设置。因此,当我们调试它暴露的故障时,我们不能只运行一个测试。

Another problem is that Chained Tests are Lonely Tests (see Erratic Test) because the current test depends on the tests that precede it to set up the test fixture. If we run the test by itself, it will likely fail because the test fixture it assumes is not set up for it. As a consequence, we cannot run just the one test when we are debugging failures it exposes.

依靠其他测试来设置测试装置必然会导致测试更难理解,因为测试装置对测试读者是不可见的——这是神秘客人的典型案例(请参阅第186页的模糊测试)。通过使用适当命名的Finder 方法(请参阅测试实用程序方法)来访问共享装置中的对象,可以至少部分缓解此问题。如果所有测试方法都在同一个测试用例类第 373页)上,并且按照执行顺序列出,则问题就不那么严重了。

Depending on other tests to set up the test fixture invariably results in tests that are more difficult to understand because the test fixture is invisible to the test reader—a classic case of a Mystery Guest (see Obscure Test on page 186). This problem can be at least partially mitigated through the use of appropriately named Finder Methods (see Test Utility Method) to access the objects in the Shared Fixture. It is less of an issue if all the Test Methods are on the same Testcase Class (page 373) and are listed in the same order as they are executed.

变体:夹具设置测试用例

如果我们需要设置一个共享装置,而又不能使用其他任何技术来设置它[例如,惰性设置第 435页)、套件装置设置第 441页)或设置装饰器第 447页)],我们可以安排将装置设置测试用例作为测试套件中的第一个测试来运行。如果我们使用测试枚举第 399页),这很容易做到;我们只需在测试套件工厂addTest中包含适当的方法调用(请参阅测试枚举)。这种变体是链式测试模式的一种退化形式,因为我们将测试套件链接在单个装置设置测试用例后面。

If we need to set up a Shared Fixture and we cannot use any of the other techniques to set it up [e.g., Lazy Setup (page 435), Suite Fixture Setup (page 441), or Setup Decorator (page 447)], we can arrange to have a Fixture Setup Testcase run as the first test in the test suite. This is simple to do if we are using Test Enumeration (page 399); we just include the appropriate addTest method call in our Test Suite Factory (see Test Enumeration). This variation is a degenerate form of the Chained Tests pattern in that we are chaining a test suite behind a single Fixture Setup Testcase.

实施说明

Implementation Notes

实施链式测试面临两个关键挑战:

There are two key challenges in implementing Chained Tests:

  • 让测试套件中的测试按所需顺序运行
  • Getting tests in the test suite to run in the desired order
  • 访问先前测试留下的装置
  • Accessing the fixture leftover by the previous test(s)

虽然 xUnit 家族的少数成员提供了定义测试顺序的明确机制,但大多数成员并未对此顺序做出这样的保证。我们可以通过进行一些实验来找出 xUnit 成员使用的顺序。最常见的是,我们会发现它要么是测试方法在文件中出现的顺序,要么是按测试方法名称的字母顺序排列(在这种情况下,最简单的解决方案是在测试名称中包含测试序列号)。在最坏的情况下,我们始终可以恢复到测试方法枚举(请参阅测试枚举),以确保测试用例对象以正确的顺序添加到测试套件中。

While a few members of the xUnit family provide an explicit mechanism for defining the order of tests, most members make no such guarantees about this order. We can probably figure out what order the xUnit member uses by performing a few experiments. Most commonly, we will discover that it is either the order in which the Test Methods appear in the file or alphabetical order by Test Method name (in which case, the easiest solution is to include a test sequence number in the test name). In the worst-case scenario, we could always revert to Test Method Enumeration (see Test Enumeration) to ensure that Testcase Objects are added to the test suite in the correct order.

要引用先前测试创建的对象,我们需要使用其中一种装置对象访问模式。如果先前的测试是同一测试用例类的测试方法,则每个测试只需将后续测试将用于访问装置的任何对象引用存储在装置保存类变量中即可。(装置保存实例变量通常在这里不起作用,因为每个测试都在单独的测试用例对象上运行,因此测试不共享实例变量。有关实例变体何时不以这种方式运行的描述,请参阅第384页的侧栏“始终存在异常” 。)

To refer to the objects created by the previous tests, we need to use one of the fixture object access patterns. If the preceding tests are Test Methods on the same Testcase Class, it is sufficient for each test to store any object references that subsequent tests will use to access the fixture in a fixture holding class variable. (Fixture holding instance variables typically won't work here because each test runs on a separate Testcase Object and, therefore, the tests don't share instance variables. See the sidebar "There's Always an Exception" on page 384 for a description of when instance variations do not behave this way.)

如果我们的测试依赖于作为套件的一部分运行的不同测试用例类的测试方法(请参阅第387页的测试套件对象),则这两种解决方案都行不通。我们最好的选择是使用测试装置注册表(请参阅第643页的测试助手)作为存储对测试使用的对象的引用的方法。测试数据库就是一个很好的例子。

If our test depends on a Test Method on a different Testcase Class being run as a part of a Suite of Suites (see Test Suite Object on page 387), neither of these solutions will work. Our best bet will be to use a Test Fixture Registry (see Test Helper on page 643) as the means to store references to the objects used by the tests. A test database is a good example.

显然,我们不希望所依赖的测试在执行完后自行清理 — — 否则我们将无法再用作测试装置。这一要求使得链式测试新鲜装置(第 311页) 方法不兼容。

Obviously, we don't want the test we are depending on to clean up after itself—that would leave nothing for us to reuse as our test fixture. That requirement makes Chained Tests incompatible with the Fresh Fixture (page 311) approach.

激励人心的例子

Motivating Example

以下是Clint Shank 在他的博客上提供的增量表格测试的示例(请参阅第607页的参数化测试):

Here's an example of an incremental Tabular Test (see Parameterized Test on page 607) provided by Clint Shank on his blog:

public class TabularTest extends TestCase {

      private Order order = new Order();

      private static final double tolerance = 0.001;



      public void testGetTotal() {

            assertEquals("initial", 0.00, order.getTotal(), tolerance);

            testAddItemAndGetTotal("first", 1, 3.00, 3.00);

            testAddItemAndGetTotal("second",3, 5.00, 18.00);

            // 等等

      }



      private void testAddItemAndGetTotal( String msg,

                                                                    int lineItemQuantity,

                                                                    double lineItemPrice,

                                                                    double expectedTotal) {

            // 设置

            LineItem item = new LineItem( lineItemQuantity,

                                                                            lineItemPrice);

            // 练习 SUT

            order.addItem(item);

            // 验证总计

            assertEquals(msg,expectedTotal,order.getTotal(),tolerance);

      }

}

public  class  TabularTest  extends  TestCase  {

      private  Order  order  =  new  Order();

      private  static  final  double  tolerance  =  0.001;



      public  void  testGetTotal()  {

            assertEquals("initial",  0.00,  order.getTotal(),  tolerance);

            testAddItemAndGetTotal("first",  1,  3.00,  3.00);

            testAddItemAndGetTotal("second",3,  5.00,  18.00);

            //  etc.

      }



      private  void  testAddItemAndGetTotal(  String  msg,

                                                                    int  lineItemQuantity,

                                                                    double  lineItemPrice,

                                                                    double  expectedTotal)  {

            //  setup

            LineItem  item  =  new  LineItem(      lineItemQuantity,

                                                                            lineItemPrice);

            //  exercise  SUT

            order.addItem(item);

            //  verify  total

            assertEquals(msg,expectedTotal,order.getTotal(),tolerance);

      }

}

 

此测试首先构建一个空订单,验证总数为零,然后继续添加几个项目,验证每个项目后的总数(图 20.3)。此测试的主要问题是,如果其中一个子测试失败,则所有后续子测试都不会运行。例如,假设舍入错误导致第二项后的总数不正确:我们是否想看看第四、第五和第六项是否仍然正确?

This test begins by building an empty order, verifies the total is zero, and then proceeds to add several items verifying the total after each item (Figure 20.3). The main issue with this test is that if one of the subtests fails, all subsequent subtests don't get run. For example, suppose a rounding error makes the total after the second item incorrect: Wouldn't we like to see whether the fourth, fifth, and six items are still correct?

图 20.3. 表格测试结果。下部窗格显示上部窗格中列出的单个表格测试方法中第一次失败的详细信息。由于失败,其余测试方法未执行。

Figure 20.3. Tabular Test results. The lower pane shows the details of the first failure inside the single Tabular Test method listed in the upper pane. Because of the failure, the rest of the test method is not executed.

图像

重构说明

Refactoring Notes

我们可以将此表格测试转换为一组链式测试,只需将单个测试方法拆分为每个子测试一个测试方法即可。一种方法是使用一系列提取方法 [Fowler] 重构来创建测试方法。这将迫使我们在第一次提取方法重构操作之前对所有局部变量使用引入字段 [JetBrains] 重构。定义完所有新测试方法后我们只需删除原始测试方法,让测试自动化框架(第 298页) 直接调用我们的新方法。2

We can convert this Tabular Test to a set of Chained Tests simply by breaking up the single Test Method into one Test Method per subtest. One way to do so is to use a series of Extract Method [Fowler] refactorings to create the Test Methods. This will force us to use an Introduce Field [JetBrains] refactoring for any local variables before the first Extract Method refactoring operation. Once we have defined all of the new Test Methods, we simply delete the original Test Method and let the Test Automation Framework (page 298) call our new methods directly.2

我们需要确保测试以相同的顺序运行。由于 JUnit 似乎按方法名称对测试用例对象进行排序,因此我们可以通过在测试方法名称中包含序列号来强制它们按正确的顺序运行。

We need to ensure the tests run in the same order. Because JUnit seems to sort the Testcase Objects by method name, we can force them into the right order by including a sequence number in the Test Method name.

最后,我们需要将Fresh Fixture转换为Shared Fixture。我们通过将order字段(实例变量)更改为类变量(Java 中的静态变量)来实现这一点,以便所有Testcase Objects都使用相同的Order

Finally, we need to convert our Fresh Fixture into a Shared Fixture. We do so by changing our order field (instance variable) into a class variable (a static variable in Java) so that all of the Testcase Objects use the same Order.

示例:链式测试

Example: Chained Tests

以下是分为三个独立测试的简单示例:

Here's the simple example turned into three separate tests:

            私有静态订单 order = new Order();

            私有静态最终双公差 = 0.001;

            公共 void test_01_initialTotalShouldBeZero() {

                  assertEquals("initial", 0.00, order.getTotal(), tolerance);

            }

            公共 void test_02_totalAfter1stItemShouldBeOnlyItemAmount(){

                  testAddItemAndGetTotal( "first", 1, 3.00, 3.00);

            }

            公共 void test_03_totalAfter2ndItemShouldBeSumOfAmounts() {

                  testAddItemAndGetTotal( "second",3, 5.00, 18.00);

            }

            私有 void testAddItemAndGetTotal( String msg,

                                                                         int lineItemQuantity,

                                                                        double lineItemPrice,

                                                                        double expectedTotal) {

                  // 创建一个订单项

                  LineItem item =

                          new LineItem(lineItemQuantity, lineItemPrice);

                  // 将项目添加到订单

                  order.addItem(item);

                  // 验证总数

                  assertEquals(msg,expectedTotal,order.getTotal(),tolerance);

            }

            private  static  Order  order  =  new  Order();

            private  static  final  double  tolerance  =  0.001;

            public  void  test_01_initialTotalShouldBeZero()  {

                  assertEquals("initial",  0.00,  order.getTotal(),  tolerance);

            }

            public  void  test_02_totalAfter1stItemShouldBeOnlyItemAmount(){

                  testAddItemAndGetTotal(  "first",  1,  3.00,  3.00);

            }

            public  void  test_03_totalAfter2ndItemShouldBeSumOfAmounts()  {

                  testAddItemAndGetTotal(  "second",3,  5.00,  18.00);

            }

            private  void  testAddItemAndGetTotal(  String  msg,

                                                                         int  lineItemQuantity,

                                                                        double  lineItemPrice,

                                                                        double  expectedTotal)  {

                  //  create  a  line  item

                  LineItem  item  =

                          new  LineItem(lineItemQuantity,  lineItemPrice);

                  //  add  line  item  to  order

                  order.addItem(item);

                  //  verify  total

                  assertEquals(msg,expectedTotal,order.getTotal(),tolerance);

            }

 

测试运行器(第377页)为我们提供了更好的概览,让我们了解哪些地方出了问题,哪些地方运行正常(图 20.4)。

The Test Runner (page 377) gives us a better overview of what is wrong and what is working (Figure 20.4).

图 20.4. 链式测试结果。上方窗格显示三种测试方法,其中两种测试通过。下方窗格显示一种失败的测试方法的详细信息。

Figure 20.4. Chained Tests result. The upper pane shows the three test methods with two tests passing. The lower pane shows the details of the one failing Test Method.

图像

不幸的是,由于测试之间的相互依赖性,我们在调试此问题时无法单独运行任何测试(除了第一个测试);它们是“孤独测试”

Unfortunately, we will not be able to run any of the tests by themselves while we debug this problem (except for the very first test) because of the interdependencies between the tests; they are Lonely Tests.

第21章

结果验证模式

Chapter 21

Result Verification Patterns

 

本章中的模式

Patterns in This Chapter

验证策略

Verification Strategy

      

州验证 462

      

State Verification 462

      

行为验证 468

      

Behavior Verification 468

断言方法样式

Assertion Method Styles

      

自定义断言 474

      

Custom Assertion 474

      

增量断言 485

      

Delta Assertion 485

      

保护断言 490

      

Guard Assertion 490

      

未完成的测试断言 494

      

Unfinished Test Assertion 494

州验证

State Verification

也称为

Also known as

基于状态的测试

State-Based Testing

当有状态需要验证时,我们如何使测试进行自我检查?

How do we make tests self-checking when there is state to be verified?

我们检查被测系统运行后的状态,并将其与预期状态进行比较。

We inspect the state of the system under test after it has been exercised and compare it to the expected state.

图像

自检测试(见第 26页)必须验证预期结果是否已发生,而无需测试人员进行手动干预。但我们所说的“预期结果”是什么意思呢?SUT 可能是也可能不是“有状态的”;如果它是有状态的,那么它在执行后可能会有也可能没有不同的状态。作为测试自动化人员,我们的工作是确定我们的预期结果是最终状态的变化,还是我们需要更具体地说明在执行 SUT时发生了什么。

A Self-Checking Test (see page 26) must verify that the expected outcome has occurred without manual intervention by whoever is running the test. But what do we mean by "expected outcome"? The SUT may or may not be "stateful"; if it is stateful, it may or may not have a different state after it has been exercised. As test automaters, it is our job to determine whether our expected outcome is a change of final state or whether we need to be more specific about what occurs while the SUT is being exercised.

状态验证涉及检查 SUT 运行之后的状态。

State Verification involves inspecting the state of the SUT after it has been exercised.

工作原理

How It Works

我们通过调用感兴趣的方法来锻炼 SUT。然后,作为一个单独的步骤,我们与 SUT 交互以检索其锻炼后的状态,并通过调用断言方法第 362页)将其与预期的最终状态进行比较。

We exercise the SUT by invoking the methods of interest. Then, as a separate step, we interact with the SUT to retrieve its post-exercise state and compare it with the expected end state by calling Assertion Methods (page 362).

通常,我们只需调用返回 SUT 状态的方法或函数即可访问 SUT 的状态。在进行测试驱动开发时尤其如此,因为测试将确保状态易于访问。然而,当我们改进测试时,我们可能会发现访问相关状态信息更具挑战性。在这些情况下,我们可能需要使用测试特定子类579页)或其他技术来公开状态,而无需在生产中引入测试逻辑(第 217页)。

Normally, we can access the state of the SUT simply by calling methods or functions that return its state. This is especially true when we are doing test-driven development because the tests will have ensured that the state is easily accessible. When we are retrofitting tests, however, we may find it more challenging to access the relevant state information. In these cases, we may need to use a Test-Specific Subclass (page 579) or some other technique to expose the state without introducing Test Logic in Production (page 217).

一个相关的问题是“SUT 的状态存储在哪里?”有时,状态存储在实际的 SUT 中;在其他情况下,状态可能存储在另一个组件(如数据库)中。在后一种情况下,状态验证可能涉及访问其他组件内的状态(本质上是跨层测试)。相比之下,行为验证(第 468页)将涉及验证 SUT 与其他组件之间的交互。

A related question is "Where is the state of the SUT stored?" Sometimes, the state is stored within the actual SUT; in other cases, the state may be stored in another component such as a database. In the latter case, State Verification may involve accessing the state within the other component (essentially a layer-crossing test). By contrast, Behavior Verification (page 468) would involve verifying the interactions between the SUT and the other component.

何时使用它

When to Use It

当我们只关心 SUT 的最终状态(而不是 SUT 如何到达该状态)时,我们应该使用状态验证。采取这种有限的视角有助于我们维护 SUT 实现的封装。

We should use State Verification when we care about only the end state of the SUT—not how the SUT got there. Taking such a limited view helps us maintain encapsulation of the implementation of the SUT.

当我们从内到外构建软件时,状态验证自然而然地出现。也就是说,我们首先构建最内层的对象,然后在其上构建下一层对象。当然,我们可能需要使用测试桩第 529)来控制 SUT 的间接输入,以避免由未经测试的代码路径导致的生产错误(第268)。即便如此,我们还是选择不验证 SUT 的间接输出。

State Verification comes naturally when we are building the software inside out. That is, we build the innermost objects first and then build the next layer of objects on top of them. Of course, we may need to use Test Stubs (page 529) to control the indirect inputs of the SUT to avoid Production Bugs (page 268) caused by untested code paths. Even then, we are choosing not to verify the indirect outputs of the SUT.

当我们确实关心在最终状态下不可见的 SUT 运行副作用(其间接输出)时,我们可以使用行为验证来直接观察行为。但是,我们必须小心,不要通过过度指定软件来创建脆弱测试第 239页)。

When we do care about the side effects of exercising the SUT that are not visible in its end state (its indirect outputs), we can use Behavior Verification to observe the behavior directly. We must be careful, however, not to create Fragile Tests (page 239) by overspecifying the software.

实施说明

Implementation Notes

实施国家验证有两种基本方式。

There are two basic styles of implementing State Verification.

变体:程序状态验证

在进行程序状态验证时,我们只需编写一系列对断言方法的调用,将状态信息分解成几部分,并将这些信息与各个预期值进行比较。大多数刚接触自动化测试的人都会选择这种“阻力最小的路径”。这种方法的主要缺点是,由于可能需要大量断言来指定预期结果,因此可能会导致测试模糊第 186页)。当必须在多个测试中或在单个测试方法中多次执行相同的断言序列时(第 348页),我们还会有测试代码重复第 213页)。

When doing Procedural State Verification, we simply write a series of calls to Assertion Methods that pick apart the state information into pieces and compare those bits of information to individual expected values. Most people who are new to automating tests take such a "path of least resistance." The major disadvantage of this approach is that it can result in Obscure Tests (page 186) owing to the number of assertions it may take to specify the expected outcome. When the same sequence of assertions must be carried out in many tests or many times within a single Test Method (page 348), we also have Test Code Duplication (page 213).

变化:预期状态规范

也称为

Also known as

预期对象

Expected Object

在执行预期状态规范时,我们以一个或多个填充了预期属性的对象的形式构建 SUT 的训练后状态规范。然后,我们使用对相等性断言的一次调用(参见断言方法)将实际状态直接与这些对象进行比较。这往往会产生更简洁、更易读的测试。每当我们需要验证多个属性时,我们都可以使用预期状态规范,并且可以构建一个看起来像我们期望 SUT 返回的对象的对象。我们需要比较的属性越多,需要比较它们的测试越多,使用预期状态规范的理由就越有说服力。在最极端的情况下,当我们有大量数据需要验证时,我们可以构建一个“预期表”并验证 SUT 是否包含它。Fit 的“行装置”提供了一种在客户测试中执行此操作的好方法;DbUnit 等工具是为此目的使用后门操作第 327页)的好方法。

When doing Expected State Specification, we construct a specification for the post-exercise state of the SUT in the form of one or more objects populated with the expected attributes. We then compare the actual state directly with these objects using a single call to an Equality Assertion (see Assertion Method). This tends to result in more concise and readable tests. We can use an Expected State Specification whenever we need to verify several attributes and it is possible to construct an object that looks like the object we expect the SUT to return. The more attributes we have that need to be compared and the more tests that need to compare them, the more compelling the argument for using an Expected State Specification. In the most extreme cases, when we have a lot of data to verify, we can construct an "expected table" and verify that the SUT contains it. Fit's "row fixtures" offer a good way to do this in customer tests; tools such as DbUnit are a good way to use Back Door Manipulation (page 327) for this purpose.

在构建预期状态规范时,我们可能更倾向于使用参数化创建方法(请参阅第415页的创建方法),这样读者就不会被预期状态规范的所有必要但不重要的属性分散注意力。预期状态规范通常是我们期望从 SUT 返回的同一类的实例。如果对象没有以涉及比较属性值的方式实现相等性(例如,通过相互比较对象引用)或者如果我们特定于测试的相等性定义与方法实现的定义不同,我们可能会难以使用预期状态规范equals

When constructing the Expected State Specification, we may prefer to use a Parameterized Creation Method (see Creation Method on page 415) so that the reader is not distracted by all the necessary but unimportant attributes of the Expected State Specification. The Expected State Specification is most often an instance of the same class that we expect to get back from the SUT. We may have difficulty using an Expected State Specification if the object doesn't implement equality in a way that involves comparing the values of attributes (e.g., by comparing the object references with each other) or if our test-specific definition of equality differs from that implemented by the equals method.

在这些情况下,如果我们创建实现特定于测试的相等性的自定义断言(第 474页),我们仍然可以使用预期状态规范。或者,我们可以从实现特定于测试的相等性的类构建预期状态规范。此类可以是覆盖方法的测试特定子类,也可以是实现的简单数据传输对象[CJ2EEP]。这两种措施都比修改(或引入)生产类上的方法更可取,因为那将是一种相等性污染(请参阅生产中的测试逻辑)。当类难以实例化时,我们可以定义一个具有必要属性的伪对象(第 551页),以及实现特定于测试的相等性的方法。最后几个“技巧”之所以成为可能,是因为相等性断言通常要求预期状态规范将其自身与实际结果进行比较,而不是相反。equalsequals(TheRealObjectClass  other)equalsequals

In these cases, we can still use an Expected State Specification if we create a Custom Assertion (page 474) that implements test-specific equality. Alternatively, we can build the Expected State Specification from a class that implements our test-specific equality. This class can either be a Test-Specific Subclass that overrides the equals method or a simple Data Transfer Object [CJ2EEP] that implements equals(TheRealObjectClass  other). Both of these measures are preferable to modifying (or introducing) the equals method on the production class, as that would be a form of Equality Pollution (see Test Logic in Production). When the class is difficult to instantiate, we can define a Fake Object (page 551) that has the necessary attributes plus an equals method that implements test-specific equality. These last few "tricks" are made possible by the fact that Equality Assertions usually ask the Expected State Specification to compare itself to the actual result, rather than the reverse.

我们可以在测试的结果验证阶段(在用于相等性断言之前)或者在测试的装置设置阶段构建预期状态规范。后一种策略允许我们在构建测试装置中的其他对象时,使用预期状态规范的属性作为传递给 SUT 的参数,或作为派生值第 718页)的基础。这使得更容易看到装置和预期状态规范之间的因果关系从而帮助我们实现测试即文档(参见第 23页)。当预期状态规范是在测试读者看不见的地方创建的(例如,使用创建方法进行构建时),这种方法特别有用。

We can build the Expected State Specification either during the result verification phase of the test immediately before it is used in the Equality Assertion or during the fixture setup phase of the test. The latter strategy allows us to use attributes of the Expected State Specification as parameters passed to the SUT or as the base for Derived Values (page 718) when building other objects in the test fixture. This makes it easier to see the cause–effect relationship between the fixture and the Expected State Specification, which in turn helps us achieve Tests as Documentation (see page 23). It is particularly useful when the Expected State Specification is created out of sight of the test reader such as when using Creation Methods to do the construction.

激励人心的例子

Motivating Example

这个简单的1示例包含一个测试,该测试执行了将行项目添加到发票的代码。由于它不包含任何断言,因此它不是自检测试

This simple 1 example features a test that exercises the code that adds a line item to an invoice. Because it contains no assertions, it is not a Self-Checking Test.

      public void testInvoice_addOneLineItem_quantity1() {

            // 练习

            inv.addItemQuantity(product, QUANTITY);

      }

      public  void  testInvoice_addOneLineItem_quantity1()  {

            //  Exercise

            inv.addItemQuantity(product,  QUANTITY);

      }

 

我们选择在方法中创建invoice一种称为“隐式设置”的方法(第 424页)。productsetUp

We have chosen to create the invoice and product in the setUp method, an approach called Implicit Setup (page 424).

    公共 void setUp() {

          product = createAnonProduct();

          anotherProduct = createAnonProduct();

          inv = createAnonInvoice();

    }

    public  void  setUp()  {

          product  =  createAnonProduct();

          anotherProduct  =  createAnonProduct();

          inv  =  createAnonInvoice();

    }

 

重构说明

Refactoring Notes

我们能做的第一个重构实际上根本不是重构,因为我们正在改变测试的行为(变得更好):我们引入一些指定预期结果的断言。这会产生一个过程状态验证的示例,因为我们在测试方法中将此更改作为对内置断言方法的一系列调用。

The first refactoring we can do is not really a refactoring at all, because we are changing the behavior of the tests (for the better): We introduce some assertions that specify the expected outcome. This results in an example of Procedural State Verification because we make this change within the Test Method as a series of calls to built-in Assertion Methods.

我们可以通过重构测试方法以使用预期对象来进一步简化测试方法。首先,我们通过构造预期类的对象或合适的测试替身第 522页)来构建预期对象,并使用先前在断言中指定的值对其进行初始化。然后,我们用单个相等断言替换一系列断言,该断言将实际结果与预期对象进行比较。如果我们需要特定于测试的相等性,我们可能必须使用自定义断言。

We can further simplify the Test Method by refactoring it to use an Expected Object. First, we build an Expected Object by constructing an object of the expected class, or a suitable Test Double (page 522), and initializing it with the values that were previously specified in the assertions. Then we replace the series of assertions with a single Equality Assertion that compares the actual result with an Expected Object. We may have to use a Custom Assertion if we need test-specific equality.

示例:程序状态验证

Example: Procedural State Verification

这里我们在测试方法中添加了断言,将其变成了自检测试。由于必须执行几个步骤来验证预期结果,因此该测试存在轻微的模糊测试问题。

Here we have added the assertions to the Test Method to turn it into a Self-Checking Test. Because several steps must be carried out to verify the expected outcome, this test suffers from a mild case of Obscure Test.

      public void testInvoice_addOneLineItem_quantity1() {

            // 练习

            inv.addItemQuantity(product, QUANTITY);

            // 验证

            列表 lineItems = inv.getLineItems();

            assertEquals("项目数量", lineItems.size(), 1);

            // 仅验证项目

            LineItem actual = (LineItem) lineItems.get(0);

            assertEquals(inv, actual.getInv());

            assertEquals(product, actual.getProd());

            assertEquals(QUANTITY, actual.getQuantity());

      }

      public  void  testInvoice_addOneLineItem_quantity1()  {

            //  Exercise

            inv.addItemQuantity(product,  QUANTITY);

            //  Verify

            List  lineItems  =  inv.getLineItems();

            assertEquals("number  of  items",  lineItems.size(),  1);

            //  Verify  only  item

            LineItem  actual  =  (LineItem)  lineItems.get(0);

            assertEquals(inv,  actual.getInv());

            assertEquals(product,  actual.getProd());

            assertEquals(QUANTITY,  actual.getQuantity());

      }

 

示例:预期对象

Example: Expected Object

在这个简化版本的测试中,我们使用具有单个相等性断言的预期对象,而不是对各个属性的一系列断言:

In this simplified version of the test, we use the Expected Object with a single Equality Assertion instead of a series of assertions on individual attributes:

      public void testInvoice_addLineItem1() {

            LineItem expItem = new LineItem(inv, product, QUANTITY);

            // 练习

            inv.addItemQuantity( expItem.getProd(),

                                                      expItem.getQuantity());

            // 验证

            列表 lineItems = inv.getLineItems();

            assertEquals("number of items", lineItems.size(), 1);

            LineItem actual = (LineItem) lineItems.get(0);

            assertEquals("Item", expItem, actual);

      }

      public  void  testInvoice_addLineItem1()  {

            LineItem  expItem  =  new  LineItem(inv,  product,  QUANTITY);

            //  Exercise

            inv.addItemQuantity(  expItem.getProd(),

                                                      expItem.getQuantity());

            //  Verify

            List  lineItems  =  inv.getLineItems();

            assertEquals("number  of  items",  lineItems.size(),  1);

            LineItem  actual  =  (LineItem)  lineItems.get(0);

            assertEquals("Item",  expItem,  actual);

      }

 

因为我们还使用某些属性作为 SUT 的参数,所以我们选择在测试的夹具设置阶段构建预期对象,并使用预期对象的属性作为 SUT 参数。

Because we are also using some of the attributes as arguments of the SUT, we have chosen to build the Expected Object during the fixture setup phase of the test and to use the attributes of the Expected Object as the SUT arguments.

行为验证

Behavior Verification

也称为

Also known as

交互测试

Interaction Testing

当没有状态需要验证时,我们如何进行自我检查?

How do we make tests self-checking when there is no state to verify?

我们捕获 SUT 发生的间接输出并将其与预期行为进行比较。

We capture the indirect outputs of the SUT as they occur and compare them to the expected behavior.

图像

自检测试(见第 26页)必须验证预期结果是否已发生,而无需测试人员进行手动干预。但我们所说的“预期结果”是什么意思呢?SUT 可能是也可能不是“有状态的”;如果它是有状态的,那么在执行之后,它可能最终处于不同的状态,也可能不处于不同的状态。SUT 还可能被期望调用其他对象或组件上的方法。

A Self-Checking Test (see page 26) must verify that the expected outcome has occurred without manual intervention by whoever is running the test. But what do we mean by "expected outcome"? The SUT may or may not be "stateful"; if it is stateful, it may or may not be expected to end up in a different state after it has been exercised. The SUT may also be expected to invoke methods on other objects or components.

行为验证涉及验证 SUT 在执行时的间接输出。

Behavior Verification involves verifying the indirect outputs of the SUT as it is being exercised.

工作原理

How It Works

每个测试不仅指定 SUT 的客户端在测试的演练 SUT 阶段如何与其交互,还指定 SUT 如何与其应依赖的组件交互。这确保 SUT 确实按规定运行,而不仅仅是最终处于正确的演练后状态。

Each test specifies not only how the client of the SUT interacts with it during the exercise SUT phase of the test, but also how the SUT interacts with the components on which it should depend. This ensures that the SUT really is behaving as specified rather than just ending up in the correct post-exercise state.

行为验证几乎总是涉及与 SUT 在运行时交互的依赖组件 (DOC) 交互或替换该组件。当 SUT 将其状态存储在 DOC 中时,行为验证状态验证(第 462) 之间的界限可能会变得有点模糊,因为这两种验证形式都涉及跨层测试。我们可以根据我们是在 DOC 中验证测试后状态(状态验证)还是在 DOC 上验证 SUT 进行的方法调用(行为验证)来区分这两种情况。

Behavior Verification almost always involves interacting with or replacing a depended-on component (DOC) with which the SUT interacts at runtime. The line between Behavior Verification and State Verification (page 462) can get a bit blurry when the SUT stores its state in the DOC because both forms of verification involve layer-crossing tests. We can distinguish between the two cases based on whether we are verifying the post-test state in the DOC (State Verification) or whether we are verifying the method calls made by the SUT on the DOC (Behavior Verification).

何时使用它

When to Use It

行为验证主要是一种用于单元测试和组件测试的技术。每当 SUT 调用其他对象或组件上的方法时,我们都可以使用行为验证。每当SUT的预期输出是瞬态的并且无法通过查看 SUT 或 DOC 的运行后状态简单地确定时,我们就必须使用行为验证。这迫使我们在这些间接输出发生时对其进行监控。

Behavior Verification is primarily a technique for unit tests and component tests. We can use Behavior Verification whenever the SUT calls methods on other objects or components. We must use Behavior Verification whenever the expected outputs of the SUT are transient and cannot be determined simply by looking at the post-exercise state of the SUT or the DOC. This forces us to monitor these indirect outputs as they occur.

行为验证的一个常见应用是当我们以“由外而内”的方式编写代码时。这种方法通常称为需求驱动开发,涉及在编写 DOC之前编写客户端代码。这是一种基于真实、具体示例而非推测来准确找出 DOC 提供的接口需要什么的好方法。这种方法的主要缺点是我们需要使用大量测试替身第 522页)来编写这些测试。这可能会导致脆弱测试第 239页),因为每个测试都非常了解 SUT 的实现方式。由于测试根据 SUT 与 DOC 的交互来指定 SUT 的行为,因此 SUT 实现的更改可能会破坏许多测试。这种过度指定的软件(请参阅脆弱测试)可能导致高昂的测试维护成本(第265页)。

A common application of Behavior Verification is when we are writing our code in an "outside-in" manner. This approach, which is often called need-driven development, involves writing the client code before we write the DOC. It is a good way to find out exactly what the interface provided by the DOC needs to be based on real, concrete examples rather than on speculation. The main objection to this approach is that we need to use a lot of Test Doubles (page 522) to write these tests. That could result in Fragile Tests (page 239) because each test knows so much about how the SUT is implemented. Because the tests specify the behavior of the SUT in terms of its interactions with the DOC, a change in the implementation of the SUT could break a lot of tests. This kind of Overspecified Software (see Fragile Test) could lead to High Test Maintenance Cost (page 265).

目前尚不清楚行为验证是否比状态验证更好。在大多数情况下,状态验证显然是必要的;在某些情况下,行为验证显然是必要的。尚待确定的是,是否应在所有情况下都使用行为验证,或者我们是否应该在大多数情况下使用状态验证,并且仅在状态验证无法完全覆盖测试时才诉诸行为验证。

The jury is still out on whether Behavior Verification is a better approach than State Verification. In most cases, State Verification is clearly necessary; in some cases, Behavior Verification is clearly necessary. What has yet to be determined is whether Behavior Verification should be used in all cases or whether we should use State Verification most of the time and resort to Behavior Verification only when State Verification falls short of full test coverage.

实施说明

Implementation Notes

在通过调用感兴趣的方法来运行 SUT 之前,我们必须确保有办法观察其行为。有时,SUT 用于与周围组件交互的机制使这种观察成为可能;当情况并非如此时,我们必须安装某种测试替身来监视 SUT 的间接输出。只要我们有办法用测试替身替换 DOC,我们就可以使用测试替身。这可以通过依赖注入第 678页)或依赖查找第 686页)来实现。

Before we exercise the SUT by invoking the methods of interest, we must ensure that we have a way of observing its behavior. Sometimes the mechanisms that the SUT uses to interact with the components surrounding it make such observation possible; when this is not the case, we must install some sort of Test Double to monitor the SUT's indirect outputs. We can use a Test Double as long as we have a way to replace the DOC with the Test Double. This could be via Dependency Injection (page 678) or by Dependency Lookup (page 686).

有两种完全不同的方法可以实现行为验证,每种方法都有自己的支持者。Mock对象(第 544页) 社区一直非常赞成使用“模拟”作为预期行为规范,因此它是更常用的方法。然而,Mock 对象并不是进行行为验证的唯一方法。

There are two fundamentally different ways to implement Behavior Verification, each with its own proponents. The Mock Object (page 544) community has been very vocal about the use of "mocks" as an Expected Behavior Specification, so it is the more commonly used approach. Nevertheless, Mock Objects are not the only way of doing Behavior Verification.

变体:程序行为验证

程序行为验证中,我们捕获 SUT 执行时进行的方法调用,然后从测试方法(第 348页)中访问它们。然后我们使用相等断言(请参阅第362页的断言方法) 将它们与预期结果进行比较。

In Procedural Behavior Verification, we capture the method calls made by the SUT as it executes and later get access to them from within the Test Method (page 348). Then we use Equality Assertions (see Assertion Method on page 362) to compare them with the expected results.

也称为

Also known as

预期行为

Expected Behavior

捕获 SUT 间接输出的最常见方法是在四阶段测试(第 358页) 的夹具设置阶段安装测试间谍(第538页) 来代替 DOC 。在测试的结果验证阶段,我们会询问测试间谍在练习 SUT 阶段是如何被 SUT 使用的。使用测试间谍不需要事先了解如何调用 DOC 的方法。

The most common way of trapping the indirect outputs of the SUT is to install a Test Spy (page 538) in place of the DOC during the fixture setup phase of the Four-Phase Test (page 358). During the result verification phase of the test, we ask the Test Spy how it was used by the SUT during the exercise SUT phase. Use of a Test Spy does not require any advance knowledge of how the methods of the DOC will be called.

另一种方法是询问真正的 DOC 如何使用它。虽然这种方案并不总是可行的,但当它可行时,它避免了使用测试替身的需要,并最大限度地减少了我们拥有过度指定软件的程度。

The alternative is to ask the real DOC how it was used. Although this scheme is not always feasible, when it is, it avoids the need to use a Test Double and minimizes the degree to which we have Overspecified Software.

我们可以通过为方法调用的参数定义预期对象(参见状态验证)或将其验证委托给自定义断言第 474页)来减少测试方法中的代码量(并避免测试代码重复;参见第213页) 。

We can reduce the amount of code in the Test Method (and avoid Test Code Duplication; see page 213) by defining Expected Objects (see State Verification) for the arguments of method calls or by delegating the verification of them to Custom Assertions (page 474).

变化:预期行为规范

预期行为规范是进行行为验证的另一种方式。我们无需等到事后再使用一系列断言来验证 SUT 的间接输出,而是将预期行为规范加载到Mock 对象中,让它在收到方法调用时验证方法调用是否正确。

Expected Behavior Specification is a different way of doing Behavior Verification. Instead of waiting until after the fact to verify the indirect outputs of the SUT by using a sequence of assertions, we load the Expected Behavior Specification into a Mock Object and let it verify that the method calls are correct as they are received.

当我们提前确切知道应该发生什么,并且希望从测试方法中删除所有程序行为验证时,我们可以使用预期行为规范。这种模式变化往往会使测试更短(假设我们使用预期行为的紧凑表示),并且可以用于在第一次偏离预期行为时导致测试失败(如果我们愿意的话)。

We can use an Expected Behavior Specification when we know exactly what should happen ahead of time and we want to remove all Procedural Behavior Verification from the Test Method. This pattern variation tends to make the test shorter (assuming we are using a compact representation of the expected behavior) and can be used to cause the test to fail on the first deviation from the expected behavior if we so choose.

使用模拟对象的一个​​明显优势是,xUnit 系列的许多成员都提供测试替身生成工具。它们使预期行为规范的实现变得非常容易,因为我们不需要为每组测试手动构建测试替身。使用模拟对象的一个​​缺点是,它要求我们能够预测 DOC 的方法将如何被调用以及在方法调用中将向其传递哪些参数。

One distinct advantage of using Mock Objects is that Test Double generation tools are available for many members of the xUnit family. They make implementing Expected Behavior Specification very easy because we don't need to manually build a Test Double for each set of tests. One drawback of using a Mock Object is that it requires that we can predict how the methods of the DOC will be called and what arguments will be passed to it in the method calls.

激励人心的例子

Motivating Example

以下测试不是自检测试,因为它不验证预期结果是否确实发生;它不包含对断言方法的调用,也不对模拟对象设置任何期望。因为我们正在测试 SUT 的日志记录功能,所以我们感兴趣的状态实际上存储在 SUT 中,logger而不是存储在 SUT 本身内。此测试的编写者尚未找到访问我们试图验证的状态的方法。

The following test is not a Self-Checking Test because it does not verify that the expected outcome has actually occurred; it contains no calls to Assertion Methods, nor does it set up any expectations on a Mock Object. Because we are testing the logging functionality of the SUT, the state that interests us is actually stored in the logger rather than within the SUT itself. The writer of this test hasn't found a way to access the state we are trying to verify.

      public void testRemoveFlightLogging_NSC() throws Exception {

            // 设置

            FlightDto expectedFlightDto = createARegisteredFlight();

            FlightManagementFacade Facade =

                      new FlightManagementFacadeImpl();

            // 执行

            Facade.removeFlight(expectedFlightDto.getFlightNumber());

            // 验证

            // 尚未找到验证结果的方法

            // 日志包含航班移除记录

      }

      public  void  testRemoveFlightLogging_NSC()  throws  Exception  {

            //  setup

            FlightDto  expectedFlightDto  =  createARegisteredFlight();

            FlightManagementFacade  facade  =

                      new  FlightManagementFacadeImpl();

            //  exercise

            facade.removeFlight(expectedFlightDto.getFlightNumber());

            //  verify

            //  have  not  found  a  way  to  verify  the  outcome  yet

            //    Log  contains  record  of  Flight  removal

      }

 

为了验证结果,运行测试的人必须访问数据库和日志控制台,并将实际输出的内容与应该输出的内容进行比较。

To verify the outcome, whoever is running the tests must access the database and the log console and compare what was actually output to what should have been output.

使测试自检的一种方法是使用SUT 的预期状态规范(参见状态验证)来增强测试,如下所示:

One way to make the test Self-Checking is to enhance the test with Expected State Specification (see State Verification) of the SUT as follows:

      public void testRemoveFlightLogging_ESS() throws Exception {

            // 固定设置

            FlightDto expectedFlightDto = createAnUnregFlight();

            FlightManagementFacadeImplTI Facade =

                      new FlightManagementFacadeImplTI();

            // 练习

            Facade.removeFlight(expectedFlightDto.getFlightNumber());

            // 验证

            assertFalse("flight 在被移除后仍然存在",

                                Facade.flightExists( expectedFlightDto.

                                                                                  getFlightNumber()));

      }

      public  void  testRemoveFlightLogging_ESS()  throws  Exception  {

            //  fixture  setup

            FlightDto  expectedFlightDto  =  createAnUnregFlight();

            FlightManagementFacadeImplTI  facade  =

                      new  FlightManagementFacadeImplTI();

            //  exercise

            facade.removeFlight(expectedFlightDto.getFlightNumber());

            //  verify

            assertFalse("flight  still  exists  after  being  removed",

                                facade.flightExists(  expectedFlightDto.

                                                                                  getFlightNumber()));

      }

 

不幸的是,这个测试没有以任何方式验证 SUT 的日志功能。它还说明了行为验证产生的一个原因:SUT 的某些功能在 SUT 本身的最终状态中是不可见的,但只有当我们在 SUT 和 DOC 之间的内部观察点拦截行为,或者我们用 SUT 与之交互的对象的状态变化来表达行为时,才能看到。

Unfortunately, this test does not verify the logging function of the SUT in any way. It also illustrates one reason why Behavior Verification came about: Some functionality of the SUT is not visible within the end state of the SUT itself, but can be seen only if we intercept the behavior at an internal observation point between the SUT and the DOC or if we express the behavior in terms of state changes for the objects with which the SUT interacts.

重构说明

Refactoring Notes

当我们对“激励示例”中的第二个代码示例进行更改时,我们实际上并没有进行重构;相反,我们添加了验证逻辑以使测试行为有所不同。但是,有几个重构案例值得讨论。

When we made the changes in the second code sample in the "Motivating Example," we weren't really refactoring; instead, we added verification logic to make the tests behave differently. There are, however, several refactoring cases that are worth discussing.

为了从状态验证重构为行为验证,我们必须执行“用测试替身替换依赖项”重构,以通过测试间谍模拟对象获得 SUT 间接输出的可见性。

To refactor from State Verification to Behavior Verification, we must do a Replace Dependency with Test Double (page 522) refactoring to gain visibility of the indirect outputs of the SUT via a Test Spy or Mock Object.

为了从预期行为规范重构程序行为验证,我们安装了测试间谍而不是模拟对象。在执行 SUT 之后,我们对测试间谍返回的值做出断言,并将它们与最初配置模拟对象(我们刚刚转换为测试间谍的对象)时最初用作参数的预期值进行比较。

To refactor from an Expected Behavior Specification to Procedural Behavior Verification, we install a Test Spy instead of the Mock Object. After exercising the SUT, we make assertions on values returned by the Test Spy and compare them with the expected values that were originally used as arguments when we initially configured the Mock Object (the one that we just converted into a Test Spy).

为了从程序行为验证重构为预期行为规范,我们使用根据测试间谍返回的值所做的断言的预期值配置一个模拟对象,并安装模拟对象而不是测试间谍

To refactor from Procedural Behavior Verification to an Expected Behavior Specification, we configure a Mock Object with the expected values from the assertions made on values returned by the Test Spy and install the Mock Object instead of the Test Spy.

示例:程序行为验证

Example: Procedural Behavior Verification

以下测试验证了创建航班的基本功能,但使用程序行为验证来验证 SUT 的间接输出。也就是说,它使用测试间谍来捕获间接输出,然后通过对断言方法进行内联调用来验证这些输出是否正确。

The following test verifies the basic functionality of creating a flight but uses Procedural Behavior Verification to verify the indirect outputs of the SUT. That is, it uses a Test Spy to capture the indirect outputs and then verifies those outputs are correct by making in-line calls to the Assertion Methods.

      public void testRemoveFlightLogging_recordingTestStub()

                    throws Exception {

            // 固定设置

            FlightDto expectedFlightDto = createAnUnregFlight();

            FlightManagementFacade Facade =

                      new FlightManagementFacadeImpl();

            // 测试替身设置

            AuditLogSpy logSpy = new AuditLogSpy();

            Facade.setAuditLog(logSpy);

            // 练习

            Facade.removeFlight(expectedFlightDto.getFlightNumber());

            // 验证

            assertEquals("呼叫次数", 1,

                               logSpy.getNumberOfCalls());

            assertEquals("操作代码",

                              Helper.REMOVE_FLIGHT_ACTION_CODE,

                               logSpy.getActionCode());

            assertEquals("日期", helper.getTodaysDateWithoutTime(),

                              logSpy.getDate());

            assertEquals("用户", Helper.TEST_USER_NAME,

                              logSpy.getUser());

            断言Equals(“详细信息”,

                              expectedFlightDto.getFlightNumber(),

                              logSpy.getDetail());

      }

      public  void  testRemoveFlightLogging_recordingTestStub()

                    throws  Exception  {

            //  fixture  setup

            FlightDto  expectedFlightDto  =  createAnUnregFlight();

            FlightManagementFacade  facade  =

                      new  FlightManagementFacadeImpl();

            //        Test  Double  setup

            AuditLogSpy  logSpy  =  new  AuditLogSpy();

            facade.setAuditLog(logSpy);

            //  exercise

            facade.removeFlight(expectedFlightDto.getFlightNumber());

            //  verify

            assertEquals("number  of  calls",  1,

                               logSpy.getNumberOfCalls());

            assertEquals("action  code",

                              Helper.REMOVE_FLIGHT_ACTION_CODE,

                               logSpy.getActionCode());

            assertEquals("date",  helper.getTodaysDateWithoutTime(),

                              logSpy.getDate());

            assertEquals("user",  Helper.TEST_USER_NAME,

                              logSpy.getUser());

            assertEquals("detail",

                              expectedFlightDto.getFlightNumber(),

                              logSpy.getDetail());

      }

 

示例:预期行为规范

Example: Expected Behavior Specification

在这个版本的测试中,我们使用 JMock 框架来定义 SUT 的预期行为。方法expectson使用预期行为规范(具体来说,是预期的日志消息)mockLog配置Mock 对象。

In this version of the test, we use the JMock framework to define the expected behavior of the SUT. The method expects on mockLog configures the Mock Object with the Expected Behavior Specification (specifically, the expected log message).

      public void testRemoveFlight_JMock() throws Exception {

            // 夹具设置

            FlightDto expectedFlightDto = createAnonRegFlight();

            FlightManagementFacade Facade =

                      new FlightManagementFacadeImpl();

            // 模拟配置

            Mock mockLog = mock(AuditLog.class);

            mockLog.expects(once()).method("logMessage")

                              .with(eq(helper.getTodaysDateWithoutTime()),

                                       eq(Helper.TEST_USER_NAME),

                                       eq(Helper.REMOVE_FLIGHT_ACTION_CODE),

                                       eq(expectedFlightDto.getFlightNumber()));

         // 模拟安装

            Facade.setAuditLog((AuditLog) mockLog.proxy());

            // 练习

            Facade.removeFlight(expectedFlightDto.getFlightNumber());

            // 验证

            // JMock 自动调用的 verify() 方法

      }

      public  void  testRemoveFlight_JMock()  throws  Exception  {

            //  fixture  setup

            FlightDto  expectedFlightDto  =  createAnonRegFlight();

            FlightManagementFacade  facade  =

                      new  FlightManagementFacadeImpl();

            //  mock  configuration

            Mock  mockLog  =  mock(AuditLog.class);

            mockLog.expects(once()).method("logMessage")

                              .with(eq(helper.getTodaysDateWithoutTime()),

                                       eq(Helper.TEST_USER_NAME),

                                       eq(Helper.REMOVE_FLIGHT_ACTION_CODE),

                                       eq(expectedFlightDto.getFlightNumber()));

         //  mock  installation

            facade.setAuditLog((AuditLog)  mockLog.proxy());

            //  exercise

            facade.removeFlight(expectedFlightDto.getFlightNumber());

            //  verify

            //  verify()  method  called  automatically  by  JMock

      }

 

自定义断言

Custom Assertion

也称为

Also known as

定制断言

Bespoke Assertion

当我们有测试特定的相等逻辑时,我们如何使测试自检?

当相同的断言逻辑出现在许多测试中时,我们如何减少测试代码重复?

我们如何避免条件测试逻辑?

How do we make tests self-checking when we have test-specific equality logic?

How do we reduce Test Code Duplication when the same assertion logic appears in many tests?

How do we avoid Conditional Test Logic?

我们创建了一个专门构建的断言方法,仅比较定义测试特定相等性的对象的属性。

We create a purpose-built Assertion Method that compares only those attributes of the object that define test-specific equality.

图像

xUnit 家族的大多数成员都提供了一组相当丰富的断言方法第 362页)。但我们迟早会发现自己会说:“如果我有一个断言可以做到...,那么这个测试就容易多了。”那么为什么不自己写呢?

Most members of the xUnit family provide a reasonably rich set of Assertion Methods (page 362). But sooner or later, we inevitably find ourselves saying, "This test would be so much easier to write if I just had an assertion that did . . . ." So why not write it ourselves?

编写自定义断言的原因有很多,但无论我们的目标是什么,技术都基本相同。我们将证明系统正常运行所需的一切复杂性隐藏在具有意图揭示名称[SBPP] 的断言方法背后。

The reasons for writing a Custom Assertion are many, but the technique is pretty much the same regardless of our goal. We hide the complexity of whatever it takes to prove the system is behaving correctly behind an Assertion Method with an Intent-Revealing Name [SBPP].

工作原理

How It Works

我们将验证某事是否为真(断言)的机制封装在意图揭示名称后面。为此,我们将测试中的所有通用断言代码分解为实现验证逻辑的自定义断言。自定义相等性断言采用两个参数:预期对象(请参阅第462页的状态验证)和实际对象。

We encapsulate the mechanics of verifying that something is true (an assertion) behind an Intent-Revealing Name. To do so, we factor out all the common assertion code within the tests into a Custom Assertion that implements the verification logic. A Custom Equality Assertion takes two parameters: an Expected Object (see State Verification on page 462) and the actual object.

自定义断言的一个关键特性是它们接收通过或失败测试所需的一切作为参数。除了导致测试失败外,它们没有任何副作用。

A key characteristic of Custom Assertions is that they receive everything they need to pass or fail the test as parameters. Other than causing the test to fail, they have no side effects.

通常,我们通过重构来创建自定义断言,即在测试中识别常见的断言模式。在测试驱动时,我们可能会直接调用不存在的自定义断言,因为这会使编写测试更容易;这种策略让我们可以专注于需要测试的 SUT 部分,而不是测试执行的机制。

Typically, we create Custom Assertions through refactoring by identifying common patterns of assertions in our tests. When test driving, we might just go ahead and call a nonexistent Custom Assertion because it makes writing our test easier; this tactic lets us focus on the part of the SUT that needs to be tested rather than the mechanics of how the test would be carried out.

何时使用它

When to Use It

当以下任何陈述为真时,我们应该考虑创建自定义断言:

We should consider creating a Custom Assertion whenever any of the following statements are true:

  • 我们发现自己在一个又一个的测试中编写(或克隆)相同的断言逻辑(测试代码重复;参见第 213页)。
  • We find ourselves writing (or cloning) the same assertion logic in test after test (Test Code Duplication; see page 213).
  • 我们发现自己在测试的结果验证部分编写了条件测试逻辑第 200页)。也就是说,我们对断言方法的调用嵌入在if语句或循环中。
  • We find ourselves writing Conditional Test Logic (page 200) in the result verification part of our tests. That is, our calls to Assertion Methods are embedded in if statements or loops.
  • 我们的测试的结果验证部分受到模糊测试第 186页)的影响,因为我们在测试中使用程序性而不是声明性结果验证。
  • The result verification parts of our tests suffer from Obscure Test (page 186) because we use procedural rather than declarative result verification in the tests.
  • 我们发现,每当断言由于没有提供足够的信息而失败时,我们都会进行“频繁调试”第 248页)。
  • We find ourselves doing Frequent Debugging (page 248) whenever assertions fail because they do not provide enough information.

将断言逻辑从测试移到自定义断言的一个关键原因是尽量减少不可测试代码(见第 44页)。将验证逻辑移到自定义断言后,我们可以编写自定义断言测试(见第474页的自定义断言)来证明验证逻辑正常工作。使用自定义断言的另一个重要好处是它们有助于避免模糊测试并使测试传达意图(见第41页)。这反过来将有助于生成强大且易于维护的测试。

A key reason for moving the assertion logic out of the tests and into Custom Assertions is to Minimize Untestable Code (see page 44). Once the verification logic has been moved into a Custom Assertion, we can write Custom Assertion Tests (see Custom Assertion on page 474) to prove the verification logic is working properly. Another important benefit of using Custom Assertions is that they help avoid Obscure Tests and make tests Communicate Intent (see page 41). That, in turn, will help produce robust, easily maintained tests.

如果验证逻辑必须与 SUT 交互才能确定实际结果,我们可以使用验证方法(参见自定义断言)而不是自定义断言。如果测试的设置和练习部分(实际/预期对象的值除外)也相同,则应考虑使用参数化测试第 607页)。自定义断言相对于这两种技术的主要优势是可重用性;相同的自定义断言可以在许多不同的情况下重复使用,因为它独立于其上下文(它与外界的唯一联系是通过其参数列表进行的)。

If the verification logic must interact with the SUT to determine the actual outcome, we can use a Verification Method (see Custom Assertion) instead of a Custom Assertion. If the setup and exercise parts of the tests are also the same except for the values of the actual/expected objects, we should consider using a Parameterized Test (page 607). The primary advantage of Custom Assertions over both of these techniques is reusability; the same Custom Assertion can be reused in many different circumstances because it is independent of its context (its only contact with the outside world occurs through its parameter list).

我们最常写的自定义断言相等性断言(参见断言方法),但没有理由我们不能写其他类型的断言。

We most commonly write Custom Assertions that are Equality Assertions (see Assertion Method), but there is no reason why we cannot write other kinds as well.

变体:自定义平等断言

对于自定义相等性断言,必须向自定义断言传递预期对象和要验证的实际对象。它还应接受断言消息第 370页),以避免玩断言轮盘赌第 224页)。此类断言本质上是一种equals作为外部方法实现的方法 [Fowler]。

For custom equality assertions, the Custom Assertion must be passed an Expected Object and the actual object to be verified. It should also take an Assertion Message (page 370) to avoid playing Assertion Roulette (page 224). Such an assertion is essentially an equals method implemented as a Foreign Method [Fowler].

变体:对象属性相等性断言

我们经常会遇到自定义断言,这些断言采用一个实际对象和几个不同的预期对象,这些预期对象需要与实际对象的特定属性进行比较。(要比较的属性集由自定义断言的名称暗示。)这些自定义断言验证方法之间的主要区别在于后者与 SUT 交互,而对象属性相等断言仅查看作为参数传入的对象。

We often run across Custom Assertions that take one actual object and several different Expected Objects that need to be compared with specific attributes of the actual object. (The set of attributes to be compared is implied by the name of the Custom Assertion.) The key difference between these Custom Assertions and a Verification Method is that the latter interacts with the SUT while the Object Attribute Equality Assertion looks only at the objects passed in as parameters.

变体:域断言

所有内置断言方法都与领域无关。自定义相等性断言可实现测试特定的相等性,但仍仅比较两个对象。另一种自定义断言有助于定义“特定领域”的高级语言(参见第41页),即领域断言

All of the built-in Assertion Methods are domain independent. Custom Equality Assertions implement test-specific equality but still compare only two objects. Another style of Custom Assertion helps contribute to the definition of a "domain-specific" Higher-Level Language (see page 41)—namely, the Domain Assertion.

领域断言是一种陈述结果断言(参见断言方法),它以领域特定术语陈述应该为真的事物。它有助于将测试提升为“商业用语”。

A Domain Assertion is a Stated Outcome Assertion (see Assertion Method) that states something that should be true in domain-specific terms. It helps elevate the test into "business-speak."

变体:诊断断言

有时,每当测试失败时,我们都会发现自己在频繁调试,因为断言只告诉我们出了问题,但没有指出具体的问题(例如,断言表明这两个对象不相等,但不清楚对象不相等的地方)。我们可以编写一种特殊的自定义断言,它可能看起来就像内置断言之一,但比内置断言提供更多关于预期值和实际值之间差异的信息,因为它特定于我们的类型。(例如,它可能会告诉我们哪些属性不同或长字符串在哪里不同。)

Sometimes we find ourselves doing Frequent Debugging whenever a test fails because the assertions tell us only that something is wrong but do not identify the specific problem (e.g., the assertions indicate these two objects are not equal but it isn't clear what isn't equal about the object). We can write a special kind of Custom Assertion that may look just like one of the built-in assertions but provide more information about what is different between the expected and actual values than a built-in assertion because it is specific to our types. (For example, it might tell us which attributes are different or where long strings differ.)

在一个项目中,我们比较包含 XML 的字符串变量。每当测试失败时,我们都必须调出两个字符串检查器并滚动查看它们以查找差异。最后,我们聪明地将逻辑包含在自定义断言中,该断言告诉我们两个 XML 字符串之间的第一个差异发生在哪里。我们花在编写诊断自定义断言上的少量时间在我们运行测试时得到了多次回报。

On one project, we were comparing string variables containing XML. Whenever a test failed, we had to bring up two string inspectors and scroll through them looking for the difference. Finally, we got smart and included the logic in a Custom Assertion that told us where the first difference between the two XML strings occurred. The small amount of time we spent writing the diagnostic custom assertion was paid back many times over as we ran our tests.

变体:验证方法

在客户测试中,验证结果的复杂性很大程度上与 SUT 交互有关。验证方法自定义断言的一种形式,它直接与 SUT 交互,从而减轻了调用者的负担。这大大简化了测试,并产生了一种更“声明性”的结果规范。编写自定义断言后,我们可以编写后续测试,这些测试会更快地产生相同的结果。在某些情况下,将测试的练习 SUT 阶段合并到验证方法中可能会很有利。这距离完整的参数化测试仅一步之遥,该测试将所有测试逻辑合并到可重用的测试实用程序方法中(第599页)。

In customer tests, a lot of the complexity of verifying the outcome is related to interacting with the SUT. Verification Methods are a form of Custom Assertions that interact directly with the SUT, thereby relieving their callers from this task. This simplifies the tests significantly and leads to a more "declarative" style of outcome specification. After the Custom Assertion has been written, we can write subsequent tests that result in the same outcome much more quickly. In some cases, it may be advantageous to incorporate even the exercise SUT phase of the test into the Verification Method. This is one step short of a full Parameterized Test that incorporates all the test logic in a reusable Test Utility Method (page 599).

实施说明

Implementation Notes

自定义断言通常作为对各种内置断言方法的一组调用来实现。根据我们计划如何在测试中使用它,我们可能还希望包含标准相等断言模板,以确保null参数的正确行为。因为自定义断言本身就是一种断言方法,所以它不应该有任何副作用,也不应该调用 SUT。(如果需要这样做,它将是一种验证方法。)

The Custom Assertion is typically implemented as a set of calls to the various built-in Assertion Methods. Depending on how we plan to use it in our tests, we may also want to include the standard Equality Assertion template to ensure correct behavior with null parameters. Because the Custom Assertion is itself an Assertion Method, it should not have any side effects, nor should it call the SUT. (If it needs to do so, it would be a Verification Method.)

变体:自定义断言测试

测试狂热者还会编写自定义断言测试(自定义断言的自检测试—参见第 26页—)来验证自定义断言。这样做的好处显而易见:增强了我们对测试的信心。在大多数情况下,编写自定义断言测试并不特别困难,因为断言方法将其所有参数作为参数。

Testing zealots would also write a Custom Assertion Test (a Self-Checking Test—see page 26—for Custom Assertions) to verify the Custom Assertion. The benefit from doing so is obvious: increased confidence in our tests. In most cases, writing Custom Assertion Tests isn't particularly difficult because Assertion Methods take all their arguments as parameters.

我们可以将自定义断言视为 SUT,只需使用各种参数调用它并验证它在正确的情况下是否失败即可。单一结果断言(参见断言方法)只需要一个测试,因为它们不接受任何参数(除了可能的断言消息)。陈述结果断言需要对每个可能的值(或边界值)进行一次测试。相等性断言需要一个测试来比较两个被认为是等价的对象,一个测试将一个对象与自身进行比较,以及对每个不等式会导致断言失败的属性进行一次测试。不影响相等性的属性可以在一个额外的测试中进行验证,因为相等性断言不应该对任何一个属性引发错误。

We can treat the Custom Assertion as the SUT simply by calling it with various arguments and verifying that it fails in the right cases. Single-Outcome Assertions (see Assertion Method) need only a single test because they don't take any parameters (other than possibly an Assertion Message). Stated Outcome Assertions need one test for each possible value (or boundary value). Equality Assertions need one test that compares two objects deemed to be equivalent, one test that compares an object with itself, and one test for each attribute whose inequality should cause the assertion to fail. Attributes that don't affect equality can be verified in one additional test because the Equality Assertion should not raise an error for any of them.

自定义断言遵循正常的简单成功测试(请参阅第348页的测试方法)和预期异常测试(请参阅测试方法)模板,但有一个细微的差别:因为断言方法是 SUT,所以四阶段测试(第358页)的练习 SUT 和验证结果阶段被合并为一个阶段。

The Custom Assertions follow the normal Simple Success Test (see Test Method on page 348) and Expected Exception Test (see Test Method) templates with one minor difference: Because the Assertion Method is the SUT, the exercise SUT and verify outcome phases of the Four-Phase Test (page 358) are combined into a single phase.

每个测试都包括设置预期对象和实际对象,然后调用自定义断言。如果对象应该是等效的,那就完成了。(第 298页描述的测试自动化框架将捕获任何断言失败并使测试失败。)对于我们预期自定义断言会失败的测试,我们可以将测试编写为预期异常测试(除了四阶段测试的练习 SUT 和验证结果阶段合并到对自定义断言的单次调用中)。

Each test consists of setting up the Expected Object and the actual object and then calling the Custom Assertion. If the objects should be equivalent, that's all there is to it. (The Test Automation Framework described on page 298 would catch any assertion failures and fail the test.) For the tests where we expect the Custom Assertion to fail, we can write the test as an Expected Exception Test (except that the exercise SUT and verify outcome phases of the Four-Phase Test are combined into the single call to the Custom Assertion).

为特定测试构建要比较的对象的最简单方法是执行类似于“一个错误属性”的操作(请参阅第718页的派生值)— 即构建第一个对象并对其进行深层复制。对于成功的测试,修改任何不应比较的属性。对于每次测试失败,修改一个成为断言失败理由的属性。

The simplest way to build the objects to be compared for a specific test is to do something similar to One Bad Attribute (see Derived Value on page 718)—that is, build the first object and make a deep copy of it. For successful tests, modify any of the attributes that should not be compared. For each test failure, modify one attribute that should be grounds for failing the assertion.

关于 xUnit 系列中一些成员可能出现的复杂情况的简要警告:如果测试运行器(第 377页) 中没有进行所有测试失败处理,即使我们在自定义断言测试fail中捕获了错误或异常,对内置断言的调用或内置断言也可能会将消息添加到失败日志中。避免此行为的唯一方法是使用“封装测试运行器”单独运行每个测试,并验证一个测试是否失败并显示预期的错误消息。

A brief warning about a possible complication in a few members of the xUnit family: If all of the test failure handling does not occur in the Test Runner (page 377), calls to fail or built-in assertions may add messages to the failure log even if we catch the error or exception in our Custom Assertion Test. The only way to circumvent this behavior is to use an "Encapsulated Test Runner" to run each test by itself and verify that the one test failed with the expected error message.

激励人心的例子

Motivating Example

在以下示例中,几种测试方法重复了相同的一系列断言:

In the following example, several test methods repeat the same series of assertions:

    public void testInvoice_addOneLineItem_quantity1_b() {

          // 练习

          inv.addItemQuantity(product, QUANTITY);

          // 验证

          列表 lineItems = inv.getLineItems();

          assertEquals("number of items", lineItems.size(), 1);

          // 仅验证项目

          LineItem expItem = new LineItem(inv, product, QUANTITY);

          LineItem actual = (LineItem)lineItems.get(0);

          assertEquals(expItem.getInv(), actual.getInv());

          assertEquals(expItem.getProd(), actual.getProd());

          assertEquals(expItem.getQuantity(), actual.getQuantity());

    }





    public void testRemoveLineItemsForProduct_oneOfTwo() {

          // 设置

          发票 inv = createAnonInvoice();

          inv.addItemQuantity(product, QUANTITY);

          inv.addItemQuantity(anotherProduct, QUANTITY);

          LineItem expItem = new LineItem(inv, product, QUANTITY);

          // 练习

          inv.removeLineItemForProduct(anotherProduct);

          // 验证

          列表 lineItems = inv.getLineItems();

          assertEquals("number of items", lineItems.size(), 1);

          LineItem actual = (LineItem)lineItems.get(0);

          assertEquals(expItem.getInv(), actual.getInv());

          assertEquals(expItem.getProd(), actual.getProd());

          assertEquals(expItem.getQuantity(), actual.getQuantity());

    }

    //

    // 添加两个行项目

    //



    public void testInvoice_addTwoLineItems_sameProduct() {

          Invoice inv = createAnonInvoice();

          LineItem expItem1 = new LineItem(inv, product, QUANTITY1);

          LineItem expItem2 = new LineItem(inv, product, QUANTITY2);

          // 练习

          inv.addItemQuantity(product, QUANTITY1);

          inv.addItemQuantity(product, QUANTITY2);

          // 验证

          列表 lineItems = inv.getLineItems();

          assertEquals("项目数量", lineItems.size(), 2);

          // 验证第一项

          LineItem actual = (LineItem)lineItems.get(0);

          assertEquals(expItem1.getInv(), actual.getInv());

          assertEquals(expItem1.getProd(), actual.getProd());

          assertEquals(expItem1.getQuantity(), actual.getQuantity());

          // 验证第二项

          actual = (LineItem)lineItems.get(1);

          断言Equals(expItem2.getInv(),实际.getInv());

          断言Equals(expItem2.getProd(),实际.getProd());

          断言Equals(expItem2.getQuantity(),实际.getQuantity());

    }

    public  void  testInvoice_addOneLineItem_quantity1_b()  {

          //  Exercise

          inv.addItemQuantity(product,  QUANTITY);

          //  Verify

          List  lineItems  =  inv.getLineItems();

          assertEquals("number  of  items",  lineItems.size(),  1);

          //  Verify  only  item

          LineItem  expItem  =  new  LineItem(inv,  product,  QUANTITY);

          LineItem  actual  =  (LineItem)lineItems.get(0);

          assertEquals(expItem.getInv(),  actual.getInv());

          assertEquals(expItem.getProd(),  actual.getProd());

          assertEquals(expItem.getQuantity(),  actual.getQuantity());

    }





    public  void  testRemoveLineItemsForProduct_oneOfTwo()  {

          //  Setup

          Invoice  inv  =  createAnonInvoice();

          inv.addItemQuantity(product,  QUANTITY);

          inv.addItemQuantity(anotherProduct,  QUANTITY);

          LineItem  expItem  =  new  LineItem(inv,  product,  QUANTITY);

          //  Exercise

          inv.removeLineItemForProduct(anotherProduct);

          //  Verify

          List  lineItems  =  inv.getLineItems();

          assertEquals("number  of  items",  lineItems.size(),  1);

          LineItem  actual  =  (LineItem)lineItems.get(0);

          assertEquals(expItem.getInv(),  actual.getInv());

          assertEquals(expItem.getProd(),  actual.getProd());

          assertEquals(expItem.getQuantity(),  actual.getQuantity());

    }

    //

    //      Adding  TWO  line  items

    //



    public  void  testInvoice_addTwoLineItems_sameProduct()  {

          Invoice  inv  =  createAnonInvoice();

          LineItem  expItem1  =  new  LineItem(inv,  product,  QUANTITY1);

          LineItem  expItem2  =  new  LineItem(inv,  product,  QUANTITY2);

          //  Exercise

          inv.addItemQuantity(product,  QUANTITY1);

          inv.addItemQuantity(product,  QUANTITY2);

          //  Verify

          List  lineItems  =  inv.getLineItems();

          assertEquals("number  of  items",  lineItems.size(),  2);

          //      Verify  first  item

          LineItem  actual  =  (LineItem)lineItems.get(0);

          assertEquals(expItem1.getInv(),  actual.getInv());

          assertEquals(expItem1.getProd(),  actual.getProd());

          assertEquals(expItem1.getQuantity(),  actual.getQuantity());

          //      Verify  second  item

          actual  =  (LineItem)lineItems.get(1);

          assertEquals(expItem2.getInv(),  actual.getInv());

          assertEquals(expItem2.getProd(),  actual.getProd());

          assertEquals(expItem2.getQuantity(),  actual.getQuantity());

    }

 

请注意,第一个测试以一系列三个断言结束,而第二个测试重复这一系列三个断言两次,每行项目一次。这显然是测试代码重复的糟糕情况。

Note that the first test ends with a series of three assertions and the second test repeats the series of three assertions twice, once for each line item. This is clearly a bad case of Test Code Duplication.

重构说明

Refactoring Notes

重构狂热者可能已经看到解决方案是对这些测试进行提取方法 [Fowler] 重构。如果我们提取所有对断言方法的常见调用,我们将只剩下每个测试中的差异。提取的方法就是我们的自定义断言。我们可能还需要引入一个预期对象来保存传递给单个断言方法的所有值,这些值将传递到要传递给自定义断言的单个对象上。

Refactoring zealots can probably see that the solution is to do an Extract Method [Fowler] refactoring on these tests. If we pull out all the common calls to Assertion Methods, we will be left with only the differences in each test. The extracted method is our Custom Assertion. We may also need to introduce an Expected Object to hold all the values that were being passed to the individual Assertion Methods on a single object to be passed to the Custom Assertion.

示例:自定义断言

Example: Custom Assertion

在此测试中,我们使用自定义断言来验证是否LineItem符合预期LineItem。出于某种原因,我们选择实现特定于测试的相等性,而不是使用标准相等性断言

In this test, we use a Custom Assertion to verify that LineItem matches the expected LineItem(s). For one reason or another, we have chosen to implement a test-specific equality rather than using a standard Equality Assertion.

    public void testInvoice_addOneLineItem_quantity1_() {

          Invoice inv = createAnonInvoice();

          LineItem expItem = new LineItem(inv, product, QUANTITY);

          // 练习

          inv.addItemQuantity(product, QUANTITY);

          // 验证

          列表 lineItems = inv.getLineItems();

          assertEquals("number of items", lineItems.size(), 1);

          // 仅验证项目

          LineItem actual = (LineItem)lineItems.get(0);

          assertLineItemsEqual("LineItem", expItem, actual);

    }



    public void testAddItemQuantity_sameProduct_() {

          Invoice inv = createAnonInvoice();

          LineItem expItem1 = new LineItem(inv, product, QUANTITY1);

          LineItem expItem2 = new LineItem(inv, product, QUANTITY2);

          // 练习

          inv.addItemQuantity(product, QUANTITY1);

          inv.addItemQuantity(product, QUANTITY2);

          // 验证

          列表 lineItems = inv.getLineItems();

          assertEquals("项目数量", lineItems.size(), 2);

          // 验证第一项

          LineItem actual = (LineItem)lineItems.get(0);

          assertLineItemsEqual("项目 1",expItem1,actual);

          // 验证第二项

          actual = (LineItem)lineItems.get(1);

          assertLineItemsEqual("项目 2",expItem2, actual);

    }

    public  void  testInvoice_addOneLineItem_quantity1_()  {

          Invoice  inv  =  createAnonInvoice();

          LineItem  expItem  =  new  LineItem(inv,  product,  QUANTITY);

          //  Exercise

          inv.addItemQuantity(product,  QUANTITY);

          //  Verify

          List  lineItems  =  inv.getLineItems();

          assertEquals("number  of  items",  lineItems.size(),  1);

          //  Verify  only  item

          LineItem  actual  =  (LineItem)lineItems.get(0);

          assertLineItemsEqual("LineItem",  expItem,  actual);

    }



    public  void  testAddItemQuantity_sameProduct_()  {

          Invoice  inv  =  createAnonInvoice();

          LineItem  expItem1  =  new  LineItem(inv,  product,  QUANTITY1);

          LineItem  expItem2  =  new  LineItem(inv,  product,  QUANTITY2);

          //  Exercise

          inv.addItemQuantity(product,  QUANTITY1);

          inv.addItemQuantity(product,  QUANTITY2);

          //  Verify

          List  lineItems  =  inv.getLineItems();

          assertEquals("number  of  items",  lineItems.size(),  2);

          //  Verify  first  item

          LineItem  actual  =  (LineItem)lineItems.get(0);

          assertLineItemsEqual("Item  1",expItem1,actual);

          //  Verify  second  item

          actual  =  (LineItem)lineItems.get(1);

          assertLineItemsEqual("Item  2",expItem2,  actual);

    }

 

测试变得更小,更能揭示意图。我们还选择将一个字符串作为参数传递给自定义断言,以表明我们正在检查哪个项目,从而避免在测试失败时玩断言轮盘赌。

The tests have become significantly smaller and more intent-revealing. We have also chosen to pass a string indicating which item we are examining as an argument to the Custom Assertion to avoid playing Assertion Roulette when a test fails.

通过以下可用的自定义断言,我们可以实现这一简化的测试:

This simplified test was made possible by having the following Custom Assertion available to us:

    静态 void assertLineItemsEqual(

                               String msg, LineItem exp, LineItem act) {

          assertEquals(msg+" Inv", exp.getInv(),act.getInv());

          assertEquals(msg+" Prod", exp.getProd(), act.getProd());

          assertEquals(msg+" Quan", exp.getQuantity(), act.getQuantity());

    }

    static  void  assertLineItemsEqual(

                               String    msg,  LineItem  exp,  LineItem  act)  {

          assertEquals(msg+"  Inv",    exp.getInv(),act.getInv());

          assertEquals(msg+"  Prod",  exp.getProd(),  act.getProd());

          assertEquals(msg+"  Quan",  exp.getQuantity(),  act.getQuantity());

    }

 

自定义断言比较的对象属性与我们在上一版测试中以内联方式比较的对象属性相同;因此测试的语义没有改变。我们还将要比较的属性的名称与消息参数连接起来以获取唯一的失败消息,这使我们能够避免在测试失败时玩断言轮盘赌。

This Custom Assertion compares the same attributes of the object as we were comparing on an in-line basis in the previous version of the test; thus the semantics of the test haven't changed. We also concatenate the name of the attribute being compared with the message parameter to get a unique failure message, which allows us to avoid playing Assertion Roulette when a test fails.

示例:域断言

Example: Domain Assertion

在下一个版本的测试中,我们进一步提升了断言的级别,以便更好地传达测试场景的预期结果:

In this next version of the test, we have further elevated the level of the assertions to better communicate the expected outcome of the test scenarios:

    public void testAddOneLineItem_quantity1() {

          Invoice inv = createAnonInvoice();

          LineItem expItem = new LineItem(inv, product, QUANTITY);

          // 练习

          inv.addItemQuantity( product, QUANTITY);

          // 验证

          assertInvoiceContainsOnlyThisLineItem( inv, expItem);

    }



    public void testRemoveLineItemsForProduct_oneOfTwo_() {

          Invoice inv = createAnonInvoice();

          inv.addItemQuantity( product, QUANTITY);

          inv.addItemQuantity( anotherProduct, QUANTITY);

          LineItem expItem = new LineItem( inv, product, QUANTITY);

          // 练习

          inv.removeLineItemForProduct( anotherProduct );

          // 验证

          assertInvoiceContainsOnlyThisLineItem( inv, expItem);

    }

    public  void  testAddOneLineItem_quantity1()  {

          Invoice  inv  =  createAnonInvoice();

          LineItem  expItem  =  new  LineItem(inv,  product,  QUANTITY);

          //  Exercise

          inv.addItemQuantity(  product,  QUANTITY);

          //  Verify

          assertInvoiceContainsOnlyThisLineItem(  inv,  expItem);

    }



    public  void  testRemoveLineItemsForProduct_oneOfTwo_()  {

          Invoice  inv  =  createAnonInvoice();

          inv.addItemQuantity(  product,  QUANTITY);

          inv.addItemQuantity(  anotherProduct,  QUANTITY);

          LineItem  expItem  =  new  LineItem(  inv,  product,  QUANTITY);

          //  Exercise

          inv.removeLineItemForProduct(  anotherProduct  );

          //  Verify

          assertInvoiceContainsOnlyThisLineItem(  inv,  expItem);

    }

 

通过提取以下域断言方法,可以实现此测试的简化版本:

This simplified version of the test was made possible by extracting the following Domain Assertion method:

    void assertInvoiceContainsOnlyThisLineItem(

                                                         Invoice inv,

                                                         LineItem expItem) {

          List lineItems = inv.getLineItems();

          assertEquals("项目数量", lineItems.size(), 1);

          LineItem actual = (LineItem)lineItems.get(0);

          assertLineItemsEqual("项目",expItem, actual);

    }

    void  assertInvoiceContainsOnlyThisLineItem(

                                                         Invoice  inv,

                                                         LineItem  expItem)  {

          List  lineItems  =  inv.getLineItems();

          assertEquals("number  of  items",  lineItems.size(),  1);

          LineItem  actual  =  (LineItem)lineItems.get(0);

          assertLineItemsEqual("item",expItem,  actual);

    }

 

此示例选择放弃将消息传递给域断言以节省一点空间。在现实生活中,我们通常会在参数列表中包含一个消息字符串,并将各个断言的消息连接到传入的消息中。有关更多详细信息,请参阅断言消息第 370页)。

This example chose to forgo passing a message to the Domain Assertion to save a bit of space. In real life, we would typically include a message string in the parameter list and concatenate the messages of the individual assertions to one passed in. See Assertion Message (page 370) for more details.

示例:验证方法

Example: Verification Method

如果几个测试的练习 SUT 和结果验证阶段几乎相同,我们可以将这两个阶段合并到可重用的自定义断言中。因为这种方法将自定义断言的语义从没有副作用的函数更改为改变 SUT 状态的操作,所以我们通常给它一个以“验证”开头的更具特色的名称。

If the exercise SUT and result verification phases of several tests are pretty much identical, we can incorporate both phases into our reusable Custom Assertion. Because this approach changes the semantics of the Custom Assertion from being just a function free of side effects to an operation that changes the state of the SUT, we usually give it a more distinctive name starting with "verify".

此版本的测试仅在调用包含测试的执行 SUT 和验证结果阶段的验证方法之前设置测试装置。最容易识别的是,调用测试中没有明显的“执行”阶段,并且存在对修改作为验证方法的参数传递的对象之一的状态的方法的调用。

This version of the test merely sets up the test fixture before calling a Verification Method that incorporates both the exercise SUT and verify outcome phases of the test. It is most easily recognized by the lack of a distinct "exercise" phase in the calling test and the presence of calls to methods that modify the state of one of the objects passed as a parameter of the Verification Method.

    public void testAddOneLineItem_quantity2() {

          Invoice inv = createAnonInvoice();

          LineItem expItem = new LineItem(inv, product, QUANTITY);

          // 练习并验证

          verifyOneLineItemCanBeAdded(inv, product, QUANTITY, expItem);

    }

    public  void  testAddOneLineItem_quantity2()  {

          Invoice  inv  =  createAnonInvoice();

          LineItem  expItem  =  new  LineItem(inv,  product,  QUANTITY);

          //  Exercise  &  Verify

          verifyOneLineItemCanBeAdded(inv,  product,  QUANTITY,  expItem);

    }

 

此示例的验证方法如下:

The Verification Method for this example looks like this:

    public void verifyOneLineItemCanBeAdded(

                          Invoice inv, Product product,

                         int QUANTITY, LineItem expItem) {

          // 练习

          inv.addItemQuantity(product, QUANTITY);

          // 验证

          assertInvoiceContainsOnlyThisLineItem(inv, expItem);

    }

    public  void  verifyOneLineItemCanBeAdded(

                          Invoice  inv,  Product  product,

                         int  QUANTITY,  LineItem  expItem)  {

          //  Exercise

          inv.addItemQuantity(product,  QUANTITY);

          //  Verify

          assertInvoiceContainsOnlyThisLineItem(inv,  expItem);

    }

 

验证方法调用“纯”自定义断言,尽管如果我们没有其他自定义断言addItemQuantity要调用,它也可以很容易地包含所有断言逻辑。请注意对参数的调用;这就是从自定义断言验证方法的inv变化。

This Verification Method calls the "pure" Custom Assertion, although it could just as easily have included all the assertion logic if we didn't have the other Custom Assertion to call. Note the call to addItemQuantity on the parameter inv; this is what changes if from a Custom Assertion to a Verification Method.

示例:自定义断言测试

Example: Custom Assertion Test

这个自定义断言并不是特别复杂,所以我们可以放心地不对其进行任何自动化测试。但是,如果它有任何复杂之处,我们可能会发现编写如下测试是值得的:

This Custom Assertion isn't particularly complicated, so we may feel comfortable without having any automated tests for it. If there is anything complex about it, however, we may find it worthwhile to write tests like these:

    public void testassertLineItemsEqual_equivalent() {

          Invoice inv = createAnonInvoice();

          LineItem item1 = new LineItem(inv, product, QUANTITY1);

          LineItem item2 = new LineItem(inv, product, QUANTITY1);

          // 练习/验证

          assertLineItemsEqual("这不应该失败",item1, item2);

    }



    public void testassertLineItemsEqual_differentInvoice() {

          Invoice inv1 = createAnonInvoice();

          Invoice inv2 = createAnonInvoice();

          LineItem item1 = new LineItem(inv1, product, QUANTITY1);

          LineItem item2 = new LineItem(inv2, product, QUANTITY1);

          // 练习/验证

          try {

                assertLineItemsEqual("Msg",item1, item2);

          } catch (AssertionFailedError e) {

                assertEquals("e.getMsg",

                                          "Invoice-expected: <123> but was <124>",

                                 e.getMessage());

       return;

          }

          fail("应该抛出异常");

    }



    public void testassertLineItemsEqual_differentQuantity() {

          Invoice inv = createAnonInvoice();

          LineItem item1 = new LineItem(inv, product, QUANTITY1);

          LineItem item2 = new LineItem(inv, product, QUANTITY2);

          // 练习/验证

          try {

                assertLineItemsEqual("Msg",item1, item2);

          } catch (AssertionFailedError e) {

                pass(); // 表示不需要断言

                return;

          }

          fail("应该抛出异常");

    }

    public  void  testassertLineItemsEqual_equivalent()  {

          Invoice  inv  =  createAnonInvoice();

          LineItem  item1  =  new  LineItem(inv,  product,  QUANTITY1);

          LineItem  item2  =  new  LineItem(inv,  product,  QUANTITY1);

          //  exercise/verify

          assertLineItemsEqual("This  should  not  fail",item1,  item2);

    }



    public  void  testassertLineItemsEqual_differentInvoice()  {

          Invoice  inv1  =  createAnonInvoice();

          Invoice  inv2  =  createAnonInvoice();

          LineItem  item1  =  new  LineItem(inv1,  product,  QUANTITY1);

          LineItem  item2  =  new  LineItem(inv2,  product,  QUANTITY1);

          //  exercise/verify

          try  {

                assertLineItemsEqual("Msg",item1,  item2);

          }  catch  (AssertionFailedError  e)  {

                assertEquals("e.getMsg",

                                          "Invoice-expected:  <123>  but  was  <124>",

                                 e.getMessage());

       return;

          }

          fail("Should  have  thrown  exception");

    }



    public  void  testassertLineItemsEqual_differentQuantity()  {

          Invoice  inv  =  createAnonInvoice();

          LineItem  item1  =  new  LineItem(inv,  product,  QUANTITY1);

          LineItem  item2  =  new  LineItem(inv,  product,  QUANTITY2);

          //  exercise/verify

          try  {

                assertLineItemsEqual("Msg",item1,  item2);

          }  catch  (AssertionFailedError  e)  {

                pass();    //  to  indicate  that  no  assertion  is  needed

                return;

          }

          fail("Should  have  thrown  exception");

    }

 

此示例包含此自定义断言所需的几个自定义断言测试。请注意,代码包括一个“等效”测试和几个“不同”测试(每个属性一个,其差异会导致测试失败)。在断言预计会失败的情况下,我们必须使用预期异常测试模板的第二种形式,因为会抛出与我们的断言方法相同的异常。在其中一个“不同”测试中,我们包含了对异常消息进行断言的示例逻辑。(虽然我为了节省空间而删减了它,但这里的例子应该可以让您了解在消息上断言的位置。)fail

This example includes a few of the Custom Assertion Tests needed for this Custom Assertion. Note that the code includes one "equivalent" and several "different" tests (one for each attribute whose difference should cause the test to fail). We have to use the second form of the Expected Exception Test template in those cases where the assertion was expected to fail, because fail throws the same exception as our assertion method. In one of the "different" tests, we have included sample logic for asserting on the exception message. (Although I've abridged it to save space, the example here should give you an idea of where to assert on the message.)

增量断言

Delta Assertion

当我们无法控制装置的初始内容时,我们如何使测试进行自我检查?

How do we make tests self-checking when we cannot control the initial contents of the fixture?

我们根据 SUT 运动前后状态的差异来指定断言。

We specify assertions based on differences between the pre- and post-exercise state of the SUT.

图像

当我们使用共享装置第 317页)时(例如测试数据库),编写断言来说明在 SUT 执行后装置的内容应该是什么可能具有挑战性。这是因为其他测试可能在装置中创建了对象,我们的断言可能会检测到这些对象,这可能会导致我们的断言失败。一种解决方案是使用数据库分区方案将当前测试与所有其他测试隔离开来(请参阅第650页的数据库沙箱)。但如果我们没有这个选项,我们该怎么办?

When we are using a Shared Fixture (page 317) such as a test database, it can be challenging to code the assertions that state what the content of the fixture should be after the SUT has been exercised. This is because other tests may have created objects in the fixture that our assertions may detect and that may cause our assertions to fail. One solution is to isolate the current test from all other tests by using a Database Partitioning Scheme (see Database Sandbox on page 650). But what can we do if this option is not available to us?

使用Delta Assertions可以让我们减少对共享装置中已存在数据的依赖。

Using Delta Assertions allows us to be less dependent on which data already exist in the Shared Fixture.

工作原理

How It Works

在执行 SUT 之前,我们会对共享装置的相关部分进行快照。执行 SUT 之后,我们会指定相对于保存的快照的断言。增量断言通常验证对象数量是否已更改了正确的数量,以及 SUT 响应我们的查询所返回的对象集合的内容是否已由预期的对象扩充。

Before exercising the SUT, we take a snapshot of relevant parts of the Shared Fixture. After exercising the SUT, we specify our assertions relative to the saved snapshot. The Delta Assertions typically verify that the number of objects has changed by the right number and that the contents of collections of objects returned by the SUT in response to our queries have been augmented by the expected objects.

何时使用它

When to Use It

当我们无法完全控制测试装置并且想要避免交互测试时,我们可以使用Delta 断言(请参阅第228页的不稳定测试)。使用Delta 断言将有助于使我们的测试对装置中的更改更具弹性。我们还可以结合使用Delta 断言隐式拆卸(第516页)来检测我们正在测试的代码中的内存或数据泄漏。有关更详细的描述,请参阅第487页的侧栏“使用 Delta 断言检测数据泄漏” 。

We can use a Delta Assertion whenever we don't have full control over the test fixture and we want to avoid Interacting Tests (see Erratic Test on page 228). Using Delta Assertions will help make our tests more resilient to changes in the fixture. We can also use Delta Assertions in concert with Implicit Teardown (page 516) to detect memory or data leaks in the code that we are testing. See the sidebar "Using Delta Assertions to Detect Data Leakage" on page 487 for a more detailed description.

当测试从同一个测试运行器(第 377接连运行时,增量断言效果很好。不幸的是,它们无法防止测试运行战争(请参阅不稳定测试),因为当测试从不同的进程同时运行时会出现此类问题。只要 SUT 和夹具的状态仅由我们自己的测试修改,增量断言就会起作用。如果其他测试并行运行 (不是在当前测试之前或之后,而是同时运行),则增量断言不足以避免测试运行战争问题。

Delta Assertions work well when tests are run one after another from the same Test Runner (page 377). Unfortunately, they cannot prevent a Test Run War (see Erratic Test) because such a problem arises when tests are run at the same time from different processes. Delta Assertions work whenever the state of the SUT and the fixture are modified only by our own test. If other tests are running in parallel (not before or after the current test, but at the same time), a Delta Assertion won't be sufficient to avoid the Test Run War problem.

实施说明

Implementation Notes

在保存共享装置或 SUT的测试前状态时,我们必须确保 SUT 不能更改我们的快照。例如,如果我们的快照由 SUT 响应查询返回的对象集合组成,我们必须执行深层复制;浅层复制仅复制对象Collection而不复制其引用的对象。浅层复制将允许 SUT 在我们执行时修改它返回给我们的对象;因此,我们将丢失用于比较测试后状态的参考快照。

When saving the pre-test state of the Shared Fixture or SUT, we must make sure that the SUT cannot change our snapshot. For example, if our snapshot consists of a collection of objects returned by the SUT in response to a query, we must perform a deep copy; a shallow copy copies only the Collection object and not the objects to which it refers. Shallow copying would allow the SUT to modify the very objects it returned to us as we exercise it; as a consequence, we would lose the reference snapshot with which we are comparing the post-test state.

我们可以用几种不同的方法确保具有正确的测试后状态。假设我们的测试添加了它计划修改的任何新对象,一种方法是首先检查结果集合 (1) 具有正确数量的项目、(2) 包含所有测试前项目以及 (3) 包含新的预期对象(请参阅第 462页的状态验证)。另一种方法是从结果集合中删除所有已保存的项目,然后将剩余的内容与新的预期对象集合进行比较。这两种方法都可以隐藏在自定义断言第 474页)或验证方法(请参阅自定义断言)后面。

We can ensure that we have the correct post-test state in several different ways. Assuming that our test adds any new objects it plans to modify, one approach is to first check that the result collection (1) has the right number of items, (2) contains all the pre-test items, and (3) contains the new Expected Objects (see State Verification on page 462). Another approach is to remove all the saved items from the result collection and then compare what remains with the collection of new expected objects. Both of these approaches can be hidden behind a Custom Assertion (page 474) or a Verification Method (see Custom Assertion).


使用 Delta 断言检测数据泄漏

很久以前,在一个遥远的项目中,我们尝试了不同的方法来在客户测试后清理我们的测试装置。我们的测试访问数据库并留下对象。这种行为导致了不可重复测试(请参阅第228页的不稳定测试)和交互测试(请参阅不稳定测试)的各种问题。我们还遭受了测试速度慢第 253页)的困扰。

最终,我们想到了一个主意,通过使用自动拆卸(第 483页) 机制注册我们在测试中创建的所有对象,从而跟踪它们。然后,我们找到了一种用伪数据库(请参阅第451页的伪对象)来桩数据库的方法。接下来,我们可以针对伪数据库或真实数据库运行相同的测试。这解决了针对伪数据库运行时的许多交互问题,尽管针对真实数据库运行测试时仍然会出现这些问题 — 测试仍然会留下对象,我们想知道原因。但首先我们必须准确地确定哪些测试有问题。

解决方案其实很简单。在我们用简单哈希表实现的虚假数据库中,我们添加了一个方法来计算对象总数。我们只需将此值保存在方法中的一个实例变量中,并将其用作传递给方法中调用的相等性断言(请参阅第 362页的断言方法setUp的预期值,以验证我们是否已正确清理所有对象。[这是使用增量断言的示例(第 485页)。] 实施这个小技巧后,我们很快就能发现哪些测试存在数据泄漏(请参阅不稳定测试)。然后,我们可以将精力集中在数量少得多的测试上。tearDown

即使在今天,我们仍然发现能够对数据库和内存运行相同的测试很有用。同样,当tearDown从我们公司特定的Testcase Superclass第 638页)继承的方法出现Delta Assertion失败时,我们仍然偶尔会看到测试失败。也许可以将相同的想法应用于检查具有手动内存管理的编程语言(例如 C++)中的内存泄漏。



Using Delta Assertions to Detect Data Leakage

A long time ago, on a project far away, we were experimenting with different ways to clean up our test fixtures after the customer tests. Our tests were accessing a database and leaving objects behind. This behavior caused all sorts of problems with Unrepeatable Tests (see Erratic Test on page 228) and Interacting Tests (see Erratic Test). We were also suffering from Slow Tests (page 253).

Eventually we hit upon the idea of keeping track of all the objects we were creating in our tests by registering them with an Automated Teardown (page 503) mechanism. Then we found a way to stub out the database with a Fake Database (see Fake Object on page 551). Next we made it possible to run the same test against either the fake database or the real one. This solved many of the interaction problems when running against the fake database, although those problems still occurred when we ran the tests against the real database—tests still left objects behind, and we wanted to know why. But first we had to determine precisely which tests were at fault.

The solution turned out to be pretty simple. In our Fake Database—which was implemented using simple hash tables—we added a method to count the total number of objects. We simply saved this value in an instance variable in the setUp method and used it as the expected value passed to an Equality Assertion (see Assertion Method on page 362) called in the tearDown method to verify that we had cleaned up all objects properly. [This is an example of using Delta Assertions (page 485).] Once we implemented this little trick, we quickly found out which tests were suffering from the Data Leak (see Erratic Test). We could then focus our efforts on a much smaller number of tests.

Even today, we find it useful to be able to run the same test against the database and in memory. Similarly, we still occasionally see a test fail when the tearDown method inherited from our company-specific Testcase Superclass (page 638) has a Delta Assertion failure. Perhaps the same idea could be applied to checking for memory leaks in programming languages with manual memory management (such as C++).


 

激励人心的例子

Motivating Example

以下测试从 SUT 中检索一些对象。然后,它将实际找到的对象与预期找到的对象进行比较。

The following test retrieves some objects from the SUT. It then compares the objects it actually found with the objects it expected to find.

    public void testGetFlightsByOriginAirport_OneOutboundFlight()

                  throws Exception {

          FlightDto expectedFlightDto =

                createNewFlightBetweenExistingAirports();

          // 练习系统

          Facade.createFlight(

                            expectedFlightDto.getOriginAirportId(),

                            expectedFlightDto.getDestinationAirportId());

          // 验证结果

          列表 flightsAtOrigin = Facade.getFlightsByOriginAirport(

                                         expectedFlightDto.getOriginAirportId());

          assertOnly1FlightInDtoList( "出发地出境航班",

                                                      expectedFlightDto,

                                                      flightsAtOrigin);

    }

    public  void  testGetFlightsByOriginAirport_OneOutboundFlight()

                  throws  Exception  {

          FlightDto  expectedFlightDto  =

                createNewFlightBetweenExistingAirports();

          //  Exercise  System

          facade.createFlight(

                            expectedFlightDto.getOriginAirportId(),

                            expectedFlightDto.getDestinationAirportId());

          //  Verify  Outcome

          List  flightsAtOrigin  =  facade.getFlightsByOriginAirport(

                                         expectedFlightDto.getOriginAirportId());

          assertOnly1FlightInDtoList(  "Outbound  flight  at  origin",

                                                      expectedFlightDto,

                                                      flightsAtOrigin);

    }

 

不幸的是,由于此测试使用了Shared Fixture,因此在此之前运行的其他测试可能也添加了对象。如果我们遇到额外的意外对象,这种行为可能会导致当前测试失败。

Unfortunately, because this test used a Shared Fixture, other tests that ran before it may have added objects as well. That behavior could cause the current test to fail if we encounter additional, unexpected objects.

重构说明

Refactoring Notes

要将测试转换为使用Delta 断言,我们必须首先对稍后将对其断言的数据(或对象集合)进行快照。接下来,我们需要修改断言以关注测试前数据/对象和测试后数据/对象之间的差异。为了避免将条件测试逻辑第 200页)引入测试方法第 348页),我们可能需要引入一个新的自定义断言。虽然我们可以使用现有断言(自定义或其他)作为起点,但我们可能必须修改它们以将测试前数据考虑在内。

To convert the test to use a Delta Assertion, we must first take a snapshot of the data (or collection of objects) we will later be asserting on. Next, we need to modify our assertions to focus on the difference between the pre-test data/objects and the post-test data/objects. To avoid introducing Conditional Test Logic (page 200) into the Test Method (page 348), we may want to introduce a new Custom Assertion. Although we may be able to use existing assertions (custom or otherwise) as a starting point, we'll probably have to modify them to take the pre-test data into account.

示例:Delta 断言

Example: Delta Assertion

在此版本的测试中,我们使用Delta Assertion来验证在执行 SUT 时添加的对象。在这里,我们验证了对象比以前多了一个,并且 SUT 返回的对象集合包括新的预期对象及其之前包含的所有对象。

In this version of the test, we use a Delta Assertion to verify the objects added when we exercised the SUT. Here we are verifying that we have one more object than before and that the collection of objects returned by the SUT includes the new Expected Object and all objects that it previously contained.

    public void testCreateFlight_Delta()

                  throws Exception {

          FlightDto expectedFlightDto =

                createNewFlightBetweenExistingAirports();

          // 记住先前的状态

          List flightsBeforeCreate =

                Facade.getFlightsByOriginAirport(

                                       expectedFlightDto.getOriginAirportId());

          // 练习系统

          Facade.createFlight(

                               expectedFlightDto.getOriginAirportId(),

                              expectedFlightDto.getDestinationAirportId());

          // 验证相对于先前状态的结果

          List flightsAfterCreate =

                Facade.getFlightsByOriginAirport(

                                expectedFlightDto.getOriginAirportId());

          assertFlightIncludedInDtoList( "新航班 ",

                                                          expectedFlightDto,

                                                          flightsAfterCreate);

          assertAllFlightsIncludedInDtoList( "先前的航班",

                                                                  flightsBeforeCreate,

                                                                  flightsAfterCreate);

          assertEquals( "创建后的航班数量",

                             flightsBeforeCreate.size()+1,

                             flightsAfterCreate.size());

    }

    public  void  testCreateFlight_Delta()

                  throws  Exception  {

          FlightDto  expectedFlightDto  =

                createNewFlightBetweenExistingAirports();

          //  Remember  prior  state

          List  flightsBeforeCreate  =

                facade.getFlightsByOriginAirport(

                                       expectedFlightDto.getOriginAirportId());

          //  Exercise  system

          facade.createFlight(

                               expectedFlightDto.getOriginAirportId(),

                              expectedFlightDto.getDestinationAirportId());

          //  Verify  outcome  relative  to  prior  state

          List  flightsAfterCreate  =

                facade.getFlightsByOriginAirport(

                                expectedFlightDto.getOriginAirportId());

          assertFlightIncludedInDtoList(  "new  flight  ",

                                                          expectedFlightDto,

                                                          flightsAfterCreate);

          assertAllFlightsIncludedInDtoList(  "previous  flights",

                                                                  flightsBeforeCreate,

                                                                  flightsAfterCreate);

          assertEquals(  "Number  of  flights  after  create",

                             flightsBeforeCreate.size()+1,

                             flightsAfterCreate.size());

    }

 

由于 SUT 返回数据传输对象[CJ2EEP],我们可以确信在执行 SUT 之前保存的对象不可能改变。我们修改了自定义断言以忽略预测试对象(通过不坚持预期对象是唯一的对象),并编写了一个新的自定义断言以确保所有预测试对象也都存在。完成此任务的另一种方法是从结果集合中删除预测试对象,然后验证是否只剩下新的预期对象

Because the SUT returns Data Transfer Objects [CJ2EEP], we can be assured that the objects we saved before exercising the SUT cannot possibly change. We have modified our Custom Assertions to ignore the pre-test objects (by not insisting that the Expected Object is the only one) and have written a new Custom Assertion that ensures all pre-test objects are also present. Another way to accomplish this task is to remove the pre-test objects from the result collection and then verify that only the new Expected Objects are left.

我省略了自定义断言的实现因为它纯粹是比较对象的练习,对理解Delta 断言模式并不重要。我们当中的“测试感染者”当然会编写由某些自定义断言测试驱动的自定义断言(请参阅自定义断言)。

I've omitted the implementation of the Custom Assertions, as it is purely an exercise in comparing objects and is not salient to understanding the Delta Assertion pattern. The "test infected" among us would, of course, write the Custom Assertions driven by some Custom Assertion Tests (see Custom Assertion).

保护断言

Guard Assertion

我们如何避免条件测试逻辑

How do we avoid Conditional Test Logic?

我们用断言替换if测试中的语句,如果不满足该断言,测试就会失败。

We replace an if statement in a test with an assertion that fails the test if not satisfied.

图像

某些验证逻辑可能会失败,因为 SUT 返回的信息未按预期初始化。当测试遇到意外问题时,它可能会产生测试错误而不是测试失败。虽然测试运行器第 377页)会尽力提供有用的诊断信息,但测试自动化程序通常可以通过检查特定条件并明确报告来做得更好。

Some verification logic may fail because information returned by the SUT is not initialized as expected. When a test encounters an unexpected problem, it may produce a test error rather than a test failure. While the Test Runner (page 377) does its best to provide useful diagnostic information, the test automater can often do better by checking for the particular condition and reporting it explicitly.

使用Guard Assertion是一种很好的方法,无需引入条件测试逻辑第 200页)。

A Guard Assertion is a good way to do so without introducing Conditional Test Logic (page 200).

工作原理

How It Works

测试要么通过,要么失败。我们通过调用断言方法第 362页)来使测试失败,如果断言的条件不满足,则停止进一步执行测试。或者,我们可以用会导致测试失败的断言来替换用于避免执行会导致测试错误的断言的条件测试逻辑。此模式还记录了我们期望 Guard断言的条件为真的事实。Guard断言的失败非常清楚地表明我们期望为真的某些条件不成立;它消除了从条件逻辑推断测试结果所需的工作量。

Tests either pass or fail. We fail tests by calling Assertion Methods (page 362) that stop the test from executing further if the assertion's condition is not met. Alternatively, we can replace Conditional Test Logic that is used to avoid executing assertions when they would cause test errors with assertions that fail the test instead. This pattern also documents the fact that we expect the condition of the Guard Assertion to be true. A failure of the Guard Assertion makes it very clear that some condition we expected to be true was not; it eliminates the effort needed to infer the test result from the conditional logic.

何时使用它

When to Use It

每当我们想避免在测试方法(第 348页) 中执行语句时,我们都应该使用Guard Assertion,因为如果在与 SUT 返回的值相关的某些条件不成立时执行这些语句,则会导致错误。我们采取这一步骤,而不是在敏感语句周围放置代码构造。通常,Guard Assertion放置在四阶段测试(第358页)的练习 SUT 和验证结果阶段之间。if  then  else  fail

We should use a Guard Assertion whenever we want to avoid executing statements in our Test Method (page 348) because they would cause an error if they were executed when some condition related to values returned by the SUT is not true. We take this step instead of putting an if  then  else  fail code construct around the sensitive statements. Normally, a Guard Assertion is placed between the exercise SUT and the verify outcome phases of a Four-Phase Test (page 358).

变体:共享装置状态断言

当测试使用共享装置(第 317页) 时,在测试开始时 (在练习 SUT 阶段之前)使用Guard Assertion也很有用,可以验证共享装置是否满足测试需求。它还可以让测试读者更清楚地知道此测试实际使用了共享装置的哪些部分;清晰度越高,实现测试即文档的可能性就越大(参见第23页)。

When the test uses a Shared Fixture (page 317), a Guard Assertion can also be useful at the beginning of the test (before the exercise SUT phase) to verify that the Shared Fixture satisfies the test's needs. It also makes it clearer to the test reader which parts of the Shared Fixture this test actually uses; the greater clarity improves the likelihood of achieving Tests as Documentation (see page 23).

实施说明

Implementation Notes

我们可以使用陈述结果断言(参见断言方法)等assertNotNil相等性断言(参见断言方法)等assertEquals作为保护断言,使测试失败并阻止执行会导致测试错误的其他语句。

We can use Stated Outcome Assertions (see Assertion Method) like assertNotNil and Equality Assertions (see Assertion Method) like assertEquals as Guard Assertions that fail the test and prevent execution of other statements that would cause test errors.

激励人心的例子

Motivating Example

请考虑以下示例:

Consider the following example:

    public void testWithConditionals() throws Exception {

          String expectedLastname = "smith";

          List foundPeople = PeopleFinder.

                    findPeopleWithLastname(expectedLastname);

          if (foundPeople != null) {

                if (foundPeople.size() == 1) {

                      Person solePerson = (Person) foundPeople.get(0);

                      assertEquals( expectedLastname,solePerson.getName());

                } else {

                      fail("列表应该只有一个元素");

                }

          } else {

                fail("列表为空");

          }

    }

    public  void  testWithConditionals()  throws  Exception  {

          String  expectedLastname  =  "smith";

          List  foundPeople  =  PeopleFinder.

                    findPeopleWithLastname(expectedLastname);

          if  (foundPeople  !=  null)  {

                if  (foundPeople.size()  ==  1)  {

                      Person  solePerson  =  (Person)  foundPeople.get(0);

                      assertEquals(  expectedLastname,solePerson.getName());

                }  else  {

                      fail("list  should  have  exactly  one  element");

                }

          }  else  {

                fail("list  is  null");

          }

    }

 

此示例包含许多作者可能出错的条件语句,例如,写成(foundPeople  ==  null)而不是(foundPeople  !=  null。在基于 C 的语言中,可能会错误地使用=而不是==,这会导致测试始终通过!

This example includes plenty of conditional statements that the author might get wrong—things like writing (foundPeople  ==  null) instead of (foundPeople  !=  null). In C-based languages, one might mistakenly use = instead of ==, which would result in the test always passing!

重构说明

Refactoring Notes

我们可以使用“用保护子句替换嵌套条件”[Fowler] 重构将这个条件测试逻辑的蜘蛛网转换为一个良好的线性语句序列。(在测试中,即使是单个条件语句也被认为太多,因此是“嵌套的”!)我们可以使用陈述结果断言来检查空对象引用和相等断言来验证集合中的对象数量。如果每个断言都得到满足,则测试继续进行。如果不满足,则测试在到达下一个语句之前以失败告终。

We can use a Replace Nested Conditional with Guard Clauses [Fowler] refactoring to transform this spider web of Conditional Test Logic into a nice linear sequence of statements. (In a test, even a single conditional statement is considered too much and hence "nested"!) We can use Stated Outcome Assertions to check for null object references and Equality Assertions to verify the number of objects in the collection. If each assertion is satisfied, the test proceeds. If they are not satisfied, the test ends in failure before it reaches the next statement.

示例:简单的 Guard 断言

Example: Simple Guard Assertion

此简化版测试用断言替换了所有条件语句。它比原始测试更短,而且更易于阅读。

This simplified version of the test replaces all conditional statements with assertions. It is shorter than the original test and much easier to read.

    public void testWithoutConditionals() throws Exception {

          String expectedLastname = "smith";

          List foundPeople = PeopleFinder.

                    findPeopleWithLastname(expectedLastname);

          assertNotNull("找到人员列表", foundPeople);

          assertEquals( "人数", 1, foundPeople.size() );

          Person solePerson = (Person) foundPeople.get(0);

          assertEquals( "姓氏",

                           expectedLastname,

                           solePerson.getName() );

    }

    public  void  testWithoutConditionals()  throws  Exception  {

          String  expectedLastname  =  "smith";

          List  foundPeople  =  PeopleFinder.

                    findPeopleWithLastname(expectedLastname);

          assertNotNull("found  people  list",  foundPeople);

          assertEquals(  "number  of  people",  1,  foundPeople.size()  );

          Person  solePerson  =  (Person)  foundPeople.get(0);

          assertEquals(  "last  name",

                           expectedLastname,

                           solePerson.getName()  );

    }

 

我们现在有一条通过此测试方法第 348页)的单一线性执行路径;它将极大地提高我们对该测试正确性的信心!

We now have a single linear execution path through this Test Method (page 348); it should improve our confidence in the correctness of this test immensely!

示例:共享 Fixture Guard 断言

Example: Shared Fixture Guard Assertion

以下是依赖于共享夹具的测试示例。如果之前的测试(甚至是之前测试运行中的此测试)修改了夹具的状态,我们的 SUT 可能会返回意外结果。可能需要花费相当多的精力才能确定问题出在测试的先决条件中,而不是 SUT 中的错误。我们可以通过在测试的夹具查找阶段使用Guard Assertion明确此测试的假设,从而避免所有这些麻烦。

Here's an example of a test that depends on a Shared Fixture. If a previous test (or even this test in a previous test run) modifies the state of the fixture, our SUT could return unexpected results. It might take a fair bit of effort to determine that the problem lies in the test's pre-conditions rather than being a bug in the SUT. We can avoid all of this trouble by making the assumptions of this test explicit through the use of a Guard Assertion during the fixture lookup phase of the test.

    public void testAddFlightsByFromAirport_OneOutboundFlight_GA()

                  throws Exception {

          // Fixture Lookup

          List flights = Facade.getFlightsByOriginAirport(

                                                 ONE_OUTBOUND_FLIGHT_AIRPORT_ID );

          // 对 Fixture 内容进行保护断言

          assertEquals( "# flights precondition", 1, flights.size());

          FlightDto firstFlight = (FlightDto) flights.get(0);

          // 练习系统

          BigDecimal flightNum = Facade.createFlight(

                                                  firstFlight.getOriginAirportId(),

                                                  firstFlight.getDestAirportId());

          // 验证结果

          FlightDto expFlight = (FlightDto) firstFlight.clone();

          expFlight.setFlightNumber( flightNum );

          List actual = Facade.getFlightsByOriginAirport(

                                                   firstFlight.getOriginAirportId());

          assertExactly2FlightsInDtoList( "出发地航班",

                                                            firstFlight,

                                                            expFlight,

                                                            actual);

    }

    public  void  testAddFlightsByFromAirport_OneOutboundFlight_GA()

                  throws  Exception  {

          //  Fixture  Lookup

          List  flights  =  facade.getFlightsByOriginAirport(

                                                 ONE_OUTBOUND_FLIGHT_AIRPORT_ID  );

          //        Guard  Assertion  on  Fixture  Contents

          assertEquals(  "#  flights  precondition",  1,  flights.size());

          FlightDto  firstFlight  =  (FlightDto)  flights.get(0);

          //  Exercise  System

          BigDecimal  flightNum  =  facade.createFlight(

                                                  firstFlight.getOriginAirportId(),

                                                  firstFlight.getDestAirportId());

          //  Verify  Outcome

          FlightDto  expFlight  =  (FlightDto)  firstFlight.clone();

          expFlight.setFlightNumber(  flightNum  );

          List  actual  =  facade.getFlightsByOriginAirport(

                                                   firstFlight.getOriginAirportId());

          assertExactly2FlightsInDtoList(  "Flights  at  origin",

                                                            firstFlight,

                                                            expFlight,

                                                            actual);

    }

 

我们现在有一种方法可以确定假设是否被违反,而无需进行大量调试!这是我们实现缺陷定位的另一种方法(参见第 22页)。这次缺陷在于测试对之前运行的测试行为的假设。

We now have a way to determine that the assumptions were violated without extensive debugging! This is another way we achieve Defect Localization (see page 22). This time the defect is in the tests' assumptions on the previously run tests' behavior.

未完成的测试断言

Unfinished Test Assertion

我们如何构建我们的测试逻辑以避免测试未完成?

How do we structure our test logic to avoid leaving tests unfinished?

我们通过执行必然失败的断言来确保不完整的测试失败。

We ensure that incomplete tests fail by executing an assertion that is guaranteed to fail.

  void testSomething() {

        // 概要:

              // 在 ... 状态下创建飞行

              // 调用 ... 方法

              // 验证飞行是否处于 ... 状态

        fail("未完成测试!");

  }

  void  testSomething()  {

        //  Outline:

              //  create  a  flight  in  ...  state

              //  call  the  ...  method

              //  verify  flight  is  in  ...  state

        fail("Unfinished  Test!");

  }

 

当我们开始为特定代码段定义测试时,通过在考虑测试条件的同时在相应的测试用例类(第 373页) 上定义测试方法(第348页) 来“粗略地”完成测试是很有用的。但是,我们确实希望确保在分心时不会意外忘记填写这些测试的主体。我们希望测试在我们完成编码之前失败。

When we start defining the tests for a particular piece of code, it is useful to "rough in" the tests by defining Test Methods (page 348) on the appropriate Testcase Class (page 373) as we think of the test conditions. We do, however, want to ensure that we don't accidentally forget to fill in the bodies of these tests if we get distracted. We want the tests to fail until we finish coding them.

包含未完成的测试断言是确保我们不会忘记的好方法。

Including an Unfinished Test Assertion is a good way to make sure we don't forget.

工作原理

How It Works

我们在定义fail每个测试方法时都放置了一次调用。该fail方法是一个单一结果断言(请参阅第 362页上的断言方法),它总是会失败。我们在运行测试时包含断言消息(第370页)“未完成的测试”,以提醒测试失败的原因。

We put a single call to fail in each Test Method as we define it. The fail method is a Single-Outcome Assertion (see Assertion Method on page 362) that always fails. We include the Assertion Message (page 370) "Unfinished Test" as a reminder of why the test fails when we do run the tests.

何时使用它

When to Use It

我们不应该故意编写任何可能意外通过的测试。失败的测试会提醒我们还有工作要做。我们可以在编写的每个测试末尾添加一个未完成的测试断言,并仅在我们对测试编码正确感到满意时将其删除,以此来提醒自己这项工作。这样做没有实际成本,但好处多多。这只是养成习惯的问题。一些 IDE 甚至可以帮助我们,让我们将未完成的测试断言放入测试方法的代码生成模板中

We should not deliberately write any tests that might accidentally pass. A failing test makes a good reminder that we still have work to do. We can remind ourselves of this work by putting an Unfinished Test Assertion at the end of every test we write and by removing it only when we are satisfied that the test is coded properly. There is no real cost to doing so, but a lot of benefit. It is just a matter of getting into the habit. Some IDEs even help us out by letting us put the Unfinished Test Assertion into the code generation template for a Test Method

如果我们需要在所有代码运行之前签入测试,我们不应该为了得到一个绿条而删除测试或未完成的测试断言,因为这可能会导致丢失测试(请参阅第268页的生产错误)。相反,如果我们的 xUnit 系列成员支持它,我们可以向测试添加一个属性;如果 xUnit 使用基于名称的测试发现第 393页),我们可以重命名测试方法;或者如果我们在套件级别使用测试枚举(第 399 页,我们可以从AllTests 套件中排除整个测试用例类(请参阅第 592页的命名测试套件[Ignore]

If we need to check in the tests before all code is working, we shouldn't remove the tests or the Unfinished Test Assertions just to get a green bar, as this could result in Lost Tests (see Production Bugs on page 268). Instead, we can add an [Ignore] attribute to the test if our member of the xUnit family supports it, rename the test method if xUnit uses name-based Test Discovery (page 393), or exclude the entire Testcase Class from the AllTests Suite (see Named Test Suite on page 592) if we are using Test Enumeration (page 399) at the suite level.

实施说明

Implementation Notes

xUnit 系列的大多数成员都已fail定义方法。如果我们使用的成员不包含该方法,则应避免将其散布assertTrue(false)到整个代码中。这种代码晦涩难懂,而且容易出错,因为它违反直觉。相反,我们应该花一点时间自己将此方法编写为自定义断言第 474页),并首先为其编写自定义断言测试(请参阅自定义断言),以确保我们做对了。

Most members of the xUnit family have a fail method already defined. If the member that we are using doesn't include it, we should avoid the temptation to sprinkle assertTrue(false) throughout our code. This kind of code is obtuse and easy to get wrong because it is counter-intuitive. Instead, we should take a minute to write this method ourselves as a Custom Assertion (page 474) and write the Custom Assertion Test (see Custom Assertion) for it first to make sure we got it right.

有些 IDE 具有自定义代码生成模板的功能。有些甚至包含包含未完成测试断言的测试方法模板。

Some IDEs include the ability to customize code generation templates. Some even include a template for a Test Method that includes an Unfinished Test Assertion.

激励人心的例子

Motivating Example

考虑以下我们正在粗略编写的测试用例类:

Consider the following Testcase Class that we are roughing in:

    public void testPull_emptyStack() {



    }



    public void testPull_oneItemOnStack () {



    }



    public void testPull_twoItemsOnStack () {

          //要做的事情:编写此测试

    }



    public void testPull_oneItemsRemainingOnStack () {

          //要做的事情:编写此测试

    }

    public  void  testPull_emptyStack()  {



    }



    public  void  testPull_oneItemOnStack  ()  {



    }



    public  void  testPull_twoItemsOnStack  ()  {

          //To  do:  Write  this  test

    }



    public  void  testPull_oneItemsRemainingOnStack  ()  {

          //To  do:  Write  this  test

    }

 

如果我们的 IDE 支持该功能,添加//  To  do:  ...注释可能会提醒我们测试仍需改进 — 但在运行测试时它不会提醒我们未完成的工作。运行此测试用例类将产生绿色条,即使我们可能根本没有实现我们的堆栈!

Including the //  To  do:  ... comments may remind us that the test still needs work if our IDE supports that feature—but it won't remind us of the unfinished work when we run the tests. Running this Testcase Class will result in a green bar even though we may not have implemented our stack at all!

重构说明

Refactoring Notes

为了实现未完成的测试断言,我们需要做的就是在粗略地编写每个测试时添加以下行:

To implement Unfinished Test Assertion all we need to do is add the following line to each test as we rough it in:

失败(“未完成的测试!”);

fail("Unfinished  Test!");

 

感叹号是可选的。创建一个自定义断言可能会更好,例如:

The exclamation mark is optional. It might be even better to create a Custom Assertion such as this one:

private void unfinishedTest() {

      fail("测试未实现!");

}

private  void  unfinishedTest()  {

      fail("Test  not  implemented!");

}

 

这样我们就可以使用 IDE 的“搜索引用”功能轻松找到所有未完成的测试断言。

This would allow us to find all the Unfinished Test Assertions easily by using the "search for references" feature of our IDE.

示例:未完成的测试断言

Example: Unfinished Test Assertion

以下是每个添加了未完成测试断言的测试:

Here are the tests with an Unfinished Test Assertion added to each one:

    公共 void testPull_emptyStack() {

          未完成测试();

    }



    公共 void testPull_oneItemOnStack () {

          未完成测试();

    }



    公共 void testPull_twoItemsOnStack() {

          未完成测试();

    }



    公共 void testPull_oneItemsRemainingOnStack () {

          未完成测试();

    }

    public  void  testPull_emptyStack()  {

          unfinishedTest();

    }



    public  void  testPull_oneItemOnStack  ()  {

          unfinishedTest();

    }



    public  void  testPull_twoItemsOnStack()  {

          unfinishedTest();

    }



    public  void  testPull_oneItemsRemainingOnStack  ()  {

          unfinishedTest();

    }

 

现在我们有了一个测试用例类,在我们完成代码编写之前,它肯定会失败。失败的测试充当了编写测试的“待办事项”列表。

Now we have a Testcase Class that is guaranteed to fail until we finish writing the code. The failing tests act as a "to do" list for writing the tests.

示例:从模板生成未完成的测试方法

Example: Unfinished Test Method Generation from Template

Eclipse(版本 3.0)是一个具有自定义模板功能的 IDE 示例。其test  method模板将以下代码插入到我们的测试用例类中:

Eclipse (version 3.0) is an example of an IDE that includes the ability to customize templates. Its test  method template inserts the following code into our Testcase Class:

public void testname() 抛出异常 {

      fail("ClassName::testname 未实现");

}

public  void  testname()  throws  Exception  {

      fail("ClassName::testname  not  implemented");

}

 

字符串“ClassName”和“testname”分别是测试用例类测试方法名称的占位符;它们由 IDE 自动填充。当我们修改签名中的测试名称时,语句中的测试名称会自动调整。要将新的测试方法fail插入类中,我们只需键入“testmethod”,然后按 CTRL-SPACEBAR。

The strings "ClassName" and "testname" are placeholders for the names of the Testcase Class and Test Method, respectively; they are filled in automatically by the IDE. As we modify the test name in the signature, the test name in the fail statement is adjusted automatically. All we have to do to insert a new Test Method into a class is to type "testmethod" and then press CTRL-SPACEBAR.

第 22 章

Fixture 拆卸模式

Chapter 22

Fixture Teardown Patterns

 

本章中的模式

Patterns in This Chapter

拆卸策略

Teardown Strategy

      

垃圾收集拆卸 500

      

Garbage-Collected Teardown 500

      

自动拆卸 503

      

Automated Teardown 503

代码组织

Code Organization

      

在线拆卸 509

      

In-line Teardown 509

      

隐式拆卸 516

      

Implicit Teardown 516

垃圾收集拆卸

Garbage-Collected Teardown

我们如何拆卸测试夹具?

How do we tear down the Test Fixture?

我们让编程语言提供的垃圾收集机制在测试结束后进行清理

We let the garbage collection mechanism provided by the programming language clean up after our test.

图像

使测试可重复且稳健的很大一部分是确保在每次测试后拆除测试装置,并为下一次测试运行创建一个新的装置。此策略称为Fresh Fixture第 311页)。在提供垃圾收集的语言中,如果我们通过本地和实例变量引用资源,大部分拆除都可以自动进行。

A large part of making tests repeatable and robust is ensuring that the test fixture is torn down after each test and a new one created for the next test run. This strategy is known as a Fresh Fixture (page 311). In languages that provide garbage collection, much of the teardown can happen automatically if we refer to resources via local and instance variables.

工作原理

How It Works

在我们的测试过程中(包括夹具设置和执行 SUT)创建的许多对象都是瞬时对象,只有在创建它们的程序中某处有对它们的引用时,它们才会保持活动状态。现代语言的垃圾收集机制使用各种算法来检测“垃圾”。但最重要的是它们如何确定某些东西不是垃圾:任何可从任何其他活动对象或全局(即静态)变量访问的对象都不会被垃圾收集。

Many of the objects created during the course of our test (including both fixture setup and exercising the SUT) are transient objects that are kept alive only as long as there is a reference to them somewhere in the program that created them. The garbage collection mechanisms of modern languages use various algorithms to detect "garbage." What is most important, though, is how they determine that something is not garbage: Any object that is reachable from any other live object or from global (i.e., static) variables will not be garbage collected.

运行测试时,测试自动化框架(第 298页) 会为测试用例类(第 373页)中的每个测试方法(第 348页) 创建一个测试用例对象(第 382页),并将这些对象添加到测试套件对象(第 387页)。每当开始新的测试运行时,框架通常会丢弃现有的测试套件并构建一个新的测试套件 (以确保所有内容都是最新的)。当旧的测试套件被丢弃时,这些测试中仅由实例变量引用的任何对象都将成为垃圾回收的候选对象。

When running our tests, the Test Automation Framework (page 298) creates a Testcase Object (page 382) for each Test Method (page 348) in our Testcase Class (page 373) and adds those objects to a Test Suite Object (page 387). Whenever a new test run is started, the framework typically throws away the existing test suite and builds a new one (to be sure everything is fresh). When the old test suite is discarded, any objects referenced only by instance variables in those tests become candidates for garbage collection.

何时使用它

When to Use It

我们应该尽可能地使用垃圾收集拆卸,因为它可以为我们节省很多精力!

We should use Garbage-Collected Teardown whenever we possibly can because it will save us a lot of effort!

如果我们的编程环境不支持垃圾回收,或者我们拥有的资源无法自动进行垃圾回收(例如,文件、套接字、数据库中的记录),则需要显式地销毁或释放这些资源。如果我们使用共享装置(第 317页),我们将无法使用垃圾回收拆卸,除非我们采取一些特殊措施来保存对装置的引用,以便当测试套件运行完毕时,该引用将超出范围。

If our programming takes place in an environment that doesn't support garbage collection, or if we have resources that aren't garbage collected automatically (e.g., files, sockets, records in a database), we'll need to destroy or free those resources explicitly. If we are using a Shared Fixture (page 317), we won't be able to use Garbage-Collected Teardown unless we do something fancy to hold the reference to the fixture in such a way that it will go out of scope when our test suite has finished running.

我们可以使用内联拆卸第 509页)、隐式拆卸第 516页)或自动拆卸(第503页)来确保它们被正确释放。

We can use In-line Teardown (page 509), Implicit Teardown (page 516), or Automated Teardown (page 503) to ensure that they are released properly.

实施说明

Implementation Notes

xUnit 系列的一些成员和一些 IDE 甚至会在每次运行测试套件时替换类。我们可能会看到此行为显示为名为“重新加载类”的选项,或者它可能被强加给我们。如果我们决定利用此功能对固定装置保存类变量执行垃圾收集拆卸,我们必须小心,因为如果我们更改 IDE 或尝试从命令行运行测试(例如,从“巡航控制”或构建脚本),我们的测试可能会停止运行。

Some members of the xUnit family and some IDEs go so far as to replace the classes each time the test suite is run. We may see this behavior show up as an option called "Reload Classes" or it may be forced upon us. We must be careful if we decide to take advantage of this feature to perform Garbage-Collected Teardown with fixture holding class variables, as our tests may stop running if we change IDEs or try running our tests from the command line (e.g., from "Cruise Control" or a build script.)

激励人心的例子

Motivating Example

以下测试在 Fixture 设置期间创建一些内存对象,并使用In-line Teardown明确销毁它们。(我们可以在这个例子中使用Implicit Teardown,但这只会让读者更难看清楚发生了什么。)

The following test creates some in-memory objects during fixture setup and explicitly destroys them using In-line Teardown. (We could have used Implicit Teardown in this example but that just makes it harder for readers to see what is going on.)

public void testCancel_proposed_UIT() {

     // 固定设置

     FlightposedFlight = createAnonymousProposedFlight();

     // 练习

     SUTposedFlight.cancel();

     // 验证结果

     try{

         assertEquals( FlightState.CANCELLED,

                              posedFlight.getStatus());

   } finally {

        // 拆除

        posedFlight.delete();

        posedFlight = null;

   }

}

public  void  testCancel_proposed_UIT()  {

     //  fixture  setup

     Flight  proposedFlight  =  createAnonymousProposedFlight();

     //  exercise  SUT

     proposedFlight.cancel();

     //  verify  outcome

     try{

         assertEquals(  FlightState.CANCELLED,

                              proposedFlight.getStatus());

   }  finally  {

        //  teardown

        proposedFlight.delete();

        proposedFlight  =  null;

   }

}

 

由于这些对象不是持久性的,因此删除的代码proposedFlight是不必要的,只会使测试更加复杂和更难理解。

Because these objects are not persistent, the code to delete the proposedFlight is unnecessary and just makes the test more complicated and harder to understand.

重构说明

Refactoring Notes

要转换为Garbage-Collected Teardown,我们只需删除不必要的清理代码。如果我们使用类变量来保存对对象的引用,我们就必须将其转换为实例变量或局部变量,这两种方法都会将我们从Shared Fixture转移到Fresh Fixture

To convert to Garbage-Collected Teardown, we need only remove the unnecessary cleanup code. If we had been using a class variable to hold the reference to the object, we would have had to convert it to either an instance variable or a local variable, both of which would have moved us from a Shared Fixture to a Fresh Fixture.

示例:垃圾收集拆卸

Example: Garbage-Collected Teardown

在这个重新设计的测试中,我们让垃圾收集拆卸为我们完成这项工作。

In this reworked test, we let Garbage-Collected Teardown do the job for us.

public void testCancel_proposed_GCT() {

    // 固定设置

    FlightposedFlight = createAnonymousProposedFlight();

    // 练习 SUTposedFlight.cancel

    ();

    // 验证结果

    assertEquals( FlightState.CANCELLED,

                         posedFlight.getStatus());

    // 拆除

    // 当posedFlight超出范围时收集垃圾

}

public  void  testCancel_proposed_GCT()  {

    //  fixture  setup

    Flight  proposedFlight  =  createAnonymousProposedFlight();

    //  exercise  SUT

    proposedFlight.cancel();

    //  verify  outcome

    assertEquals(  FlightState.CANCELLED,

                         proposedFlight.getStatus());

    //  teardown

    //    Garbage  collected  when  proposedFlight  goes  out  of  scope

}

 

请注意测试变得多么简单了!

Note how much simpler the test has become!

自动拆卸

Automated Teardown

也称为

Also known as

测试对象注册表

Test Object Registry

我们如何拆卸测试夹具?

How do we tear down the Test Fixture?

我们跟踪测试中创建的所有资源,并在拆卸期间自动销毁/释放它们

We keep track of all resources that are created in a test and automatically destroy/free them during teardown.

图像

要使测试可重复且稳健,很大一部分工作就是确保每次测试后拆除测试装置,并为下一次测试运行创建一个新的装置。此策略称为Fresh Fixture第 311页)。剩余的对象和数据库记录以及打开的文件和连接,在最好的情况下会导致性能下降,在最坏的情况下会导致测试失败或系统崩溃。虽然其中一些资源可能会通过垃圾回收自动清理,但如果没有明确拆除,其他资源可能会被搁置。

A large part of making tests repeatable and robust is ensuring that the test fixture is torn down after each test and a new one created for the next test run. This strategy is known as a Fresh Fixture (page 311). Leftover objects and database records, as well as open files and connections, can at best cause performance degradations and at worst cause tests to fail or systems to crash. While some of these resources may be cleaned up automatically by garbage collection, others may be left hanging if they are not torn down explicitly.

编写在所有可能情况下都可信赖的、能够正确清理的拆卸代码是一项挑战,而且耗时。它需要了解测试的每个可能结果可能遗留什么,并编写代码来处理该场景。这种复杂的拆卸(请参阅第 186页的模糊测试)引入了相当多的条件测试逻辑第 200页)和最糟糕的不可测试的测试代码(请参阅第 209页的难以测试的代码)。

Writing teardown code that can be relied upon to clean up properly in all possible circumstances is challenging and time-consuming. It involves understanding what could be left over for each possible outcome of the test and writing code to deal with that scenario. This Complex Teardown (see Obscure Test on page 186) introduces a fair bit of Conditional Test Logic (page 200) and—worst of all—Untestable Test Code (see Hard-to-Test Code on page 209).

更好的解决方案是让测试基础设施跟踪创建的对象并在测试完成时自动清理它们。

A better solution is to let the test infrastructure track the objects created and clean them up auto-magically when the test is complete.

工作原理

How It Works

该解决方案的核心是一种机制,用于注册我们在测试中创建的每个持久项(即对象、记录、连接等)。我们维护一个(或多个)已注册对象的列表,这些对象需要采取一些措施来销毁它们。这可以像将每个对象扔进一个集合一样简单。在测试结束时,我们遍历该集合并销毁每个对象。我们希望捕获遇到的任何错误,以便一个对象的清理错误不会导致其余的清理被中止。

The core of the solution is a mechanism to register each persistent item (i.e., object, record, connection, and so on) we create in the test. We maintain a list (or lists) of registered objects that need some action to be taken to destroy them. This can be as simple as tossing each object into a collection. At the end of the test, we traverse the collection and destroy each object. We will want to catch any errors that we encounter so that one object's cleanup error will not cause the rest of the cleanup to be aborted.

何时使用它

When to Use It

当我们有需要清理的持久资源以保持测试环境正常运行时,我们可以使用自动拆卸。(这种情况在客户测试中比在单元测试中更常见。)此模式通过防止在一次测试中创建的对象停留在后续测试的执行中,解决了不可重复测试(请参阅第 228页的不稳定测试)和交互测试(请参阅不稳定测试)。

We can use Automated Teardown whenever we have persistent resources that need to be cleaned up to keep the test environment functioning. (This happens more often in customer tests than in unit tests.) This pattern addresses both Unrepeatable Tests (see Erratic Test on page 228) and Interacting Tests (see Erratic Test) by keeping the objects created in one test from lingering into the execution of a subsequent test.

自动拆卸并不难实现,而且可以为我们省去很多麻烦和精力。一旦我们为一个项目构建了它,我们就应该能够在后续项目中轻松重用拆卸逻辑。

Automated Teardown isn't very difficult to build, and it will save us a large amount of grief and effort. Once we have built it for one project, we should be able to reuse the teardown logic on subsequent projects for very little effort.

实施说明

Implementation Notes

自动拆卸有两种形式。基本版本仅拆卸作为装置设置的一部分创建的对象。更高级的版本还会销毁 SUT 在执行过程中创建的任何对象。

Automated Teardown comes in two flavors. The basic flavor tears down only objects that were created as part of fixture setup. The more advanced version also destroys any objects that were created by the SUT while it was being exercised.

变化:自动夹具拆卸

最简单的解决方案是将我们创建的对象注册到我们的创建方法中(第 415页)。虽然此模式不会拆除 SUT 创建的对象,但通过处理我们的装置,它可以大大减少工作量和出错的可能性。

The simplest solution is to register the objects we create in our Creation Methods (page 415). Although this pattern will not tear down the objects created by the SUT, by dealing with our fixture it reduces the effort and likelihood of errors significantly.

这种变化有两个关键挑战:

There are two key challenges with this variation:

  • 寻找一种通用方法来清理已注册的对象
  • Finding a generic way to clean up the registered objects
  • 确保我们的自动拆卸代码针对每个注册对象运行
  • Ensuring that our Automated Teardown code is run for each registered object

鉴于后者比较容易解决,我们先处理它。拆除持久性新鲜夹具(请参阅新鲜夹具)时,最简单的解决方案是将对自动拆除机制的调用放入测试用例类第 373tearDown页)的方法中。只要方法成功,无论测试通过还是失败,都会调用此方法。拆除共享夹具(第 317 页)时,我们希望仅在运行所有测试方法第 348页)之后才运行该方法。在这种情况下,我们可以使用套件夹具设置第 441页)(如果我们的 xUnit 系列成员支持它)或设置装饰器(第447页)。setUptearDown

Given that the latter challenge is the easier problem, let us deal with it first. When we are tearing down a Persistent Fresh Fixture (see Fresh Fixture), the simplest solution is to put the call to the Automated Teardown mechanism into the tearDown method on our Testcase Class (page 373). This method is called regardless of whether the test passes or fails as long as the setUp method succeeds. When we are tearing down a Shared Fixture (page 317), we want the tearDown method to run only after all the Test Methods (page 348) have been run. In this case, we can use either Suite Fixture Setup (page 441), if our member of the xUnit family supports it, or a Setup Decorator (page 447).

现在让我们回到更难的问题:清理资源的通用机制。我们至少有两个选择。首先,我们可以确保所有持久性(非垃圾收集)对象都实现一个通用清理机制,我们可以从自动拆卸机制中调用该机制。或者,我们可以将每个对象包装在另一个知道如何清理相关对象的对象中。后一种策略是命令[GOF]模式的一个示例。

Now let's go back to the harder problem: the generic mechanism for cleaning up the resources. We have at least two options here. First, we can ensure that all persistent (non-garbage-collected) objects implement a generic cleanup mechanism that we can call from within the Automated Teardown mechanism. Alternatively, we can wrap each object in another object that knows how to clean up the object in question. The latter strategy is an example of the Command [GOF] pattern.

如果我们以完全通用的方式构建自动拆卸机制,我们可以将其包含在测试用例超类(第638页)中,我们可以以此为基础构建所有测试用例类。否则,我们可能需要将其放在测试助手第 643页)上,该助手对所有需要它的测试用例类都可见。既能创建夹具对象又能自动拆卸它们的测试助手有时被称为对象母体(请参阅测试助手)。

If we build our Automated Teardown mechanism in a completely generic way, we can include it in the Testcase Superclass (page 638) on which we can base all our Testcase Classes. Otherwise, we may need to put it onto a Test Helper (page 643) that is visible from all Testcase Classes that need it. A Test Helper that both creates fixture objects and tears them down automatically is sometimes called an Object Mother (see Test Helper).

作为一段不平凡(且非常关键)的代码,自动拆卸机制值得拥有自己的单元测试。由于它现在不在测试方法中,我们可以为其编写自检测试(参见第26页)!如果我们真的要小心(有些人可能会说有点偏执),我们可以使用Delta 断言第 485页)来验证拆卸操作后仍然存在的任何对象在执行测试之前是否确实存在。

Being a nontrivial (and very critical) piece of code, the Automated Teardown mechanism deserves its own unit tests. Because it is now outside the Test Method, we can write Self-Checking Tests (see page 26) for it! If we want to be really careful (some might say paranoid), we can use Delta Assertions (page 485) to verify that any objects that persist after the teardown operation really existed before the test was performed.

变体:自动练习拆卸

我们还可以清理 SUT 创建的对象,从而使测试更加“自我清理”。这项工作涉及使用可观察的对象工厂(请参阅第686页的依赖项查找)设计 SUT,以便我们可以在 SUT 运行时自动注册由 SUT 创建的任何对象。在拆卸阶段,我们也可以删除这些对象。

We can make the tests even more "self-cleaning" by also cleaning up the objects created by the SUT. This effort involves designing the SUT using an observable Object Factory (see Dependency Lookup on page 686) so that we can automatically register any objects created by the SUT while it is being exercised. During the teardown phase we can delete these objects, too.

激励人心的例子

Motivating Example

在此示例中,我们使用创建方法创建了多个对象,并需要在测试完成时拆除它们。为此,我们引入了一个try/finally块,以确保即使断言失败,我们的内联拆除第 509页)代码也会执行。

In this example, we create several objects using Creation Methods and need to tear them down when the test in complete. To do so, we introduce a try/finally block to ensure that our In-line Teardown (page 509) code executes even when the assertions fail.

public void testGetFlightsByOrigin_NoInboundFlight_SMRTD()

              throws Exception {

    // Fixture Setup

    BigDecimal outboundAirport = createTestAirport("1OF");

    BigDecimal inboundAirport = null;

    FlightDto expFlightDto = null;

    try {

        inboundAirport = createTestAirport("1IF");

        expFlightDto = createTestFlight(outboundAirport, inboundAirport);

        // 练习系统

        列表 flightsAtDestination1 =

                Facade.getFlightsByOriginAirport(inboundAirport);

        // 验证结果

        assertEquals(0,flightsAtDestination1.size());

   } finally {

        try {

             Facade.removeFlight(expFlightDto.getFlightNumber());

        } finally {

             try {

                 Facade.removeAirport(inboundAirport);

             } finally {

                 Facade.removeAirport(outboundAirport);

             }

        }

   }

}

public  void  testGetFlightsByOrigin_NoInboundFlight_SMRTD()

              throws  Exception  {

    //  Fixture  Setup

    BigDecimal  outboundAirport  =  createTestAirport("1OF");

    BigDecimal  inboundAirport  =  null;

    FlightDto  expFlightDto  =  null;

    try  {

        inboundAirport  =  createTestAirport("1IF");

        expFlightDto  =  createTestFlight(outboundAirport,  inboundAirport);

        //  Exercise  System

        List  flightsAtDestination1  =

                facade.getFlightsByOriginAirport(inboundAirport);

        //  Verify  Outcome

        assertEquals(0,flightsAtDestination1.size());

   }  finally  {

        try  {

             facade.removeFlight(expFlightDto.getFlightNumber());

        } finally  {

             try  {

                 facade.removeAirport(inboundAirport);

             }  finally    {

                 facade.removeAirport(outboundAirport);

             }

        }

   }

}

 

请注意,我们必须在块中使用嵌套try/finally结构finally,以确保拆卸中的任何错误不会阻止我们完成工作。

Note that we must use nested try/finally constructs within the finally block to ensure that any errors in the teardown don't keep us from finishing the job.

重构说明

Refactoring Notes

引入自动拆卸涉及两个步骤。首先,我们将自动拆卸机制添加到我们的测试用例类中。其次,我们从测试中删除所有内联拆卸代码。

Introducing Automated Teardown involves two steps. First, we add the Automated Teardown mechanism to our Testcase Class. Second, we remove any In-line Teardown code from our tests.

自动拆卸可以在特定的测试用例类上实现,也可以通过通用类继承(或混合)。无论哪种方式,我们都需要确保注册所有新创建的对象,以便机制知道在测试完成时删除它们。这最容易在已经存在的创建方法中完成。或者,我们可以使用提取方法 [Fowler] 重构将直接构造函数调用移到新创建的创建方法中并添加注册。

Automated Teardown can be implemented on a specific Testcase Class or it can be inherited (or mixed in) via a generic class. Either way, we need to make sure we register all of our newly created objects so that the mechanism knows to delete them when the test is finished. This is most easily done inside Creation Methods that already exist. Alternatively, we can use an Extract Method [Fowler] refactoring to move the direct constructor calls into newly created Creation Methods and add the registration.

通用的自动拆卸机制应从方法中调用。虽然这可以在我们自己的测试用例类tearDown上完成,但将此方法放在我们所有测试用例类都继承自的测试用例超类上几乎总是更好的选择。如果我们还没有测试用例超类,我们可以通过执行提取类 [Fowler] 重构,然后对与自动拆卸机制相关的任何方法(和字段)执行提取方法 [Fowler] 重构,轻松创建一个。

The generic Automated Teardown mechanism should be invoked from the tearDown method. Although this can be done on our own Testcase Class, it is almost always better to put this method on a Testcase Superclass that all our Testcase Classes inherit from. If we don't already have a Testcase Superclass, we can easily create one by doing an Extract Class [Fowler] refactoring and then doing a Pull Up Method [Fowler] refactoring on any methods (and fields) associated with the Automated Teardown mechanism.

示例:自动拆卸

Example: Automated Teardown

在这个重构的测试中没有什么可看的,因为所有的拆卸代码已经被删除了。

There is not much to see in this refactored test because all of the teardown code has been removed.

public void testGetFlightsByOriginAirport_OneOutboundFlight()

throws Exception {

    // Fixture Setup

    BigDecimal outboundAirport = createTestAirport("1OF");

    BigDecimal inboundAirport = createTestAirport("1IF");

    FlightDto expectedFlightDto =

            createTestFlight( outboundAirport, inboundAirport);

    // 练习系统

    列表 flightsAtOrigin =

            Facade.getFlightsByOriginAirport(outboundAirport);

    // 验证结果

    assertOnly1FlightInDtoList( "Flights at origin",

                                            expectedFlightDto,

                                            flightsAtOrigin);

}

public  void  testGetFlightsByOriginAirport_OneOutboundFlight()

throws  Exception  {

    //  Fixture  Setup

    BigDecimal  outboundAirport  =  createTestAirport("1OF");

    BigDecimal  inboundAirport  =  createTestAirport("1IF");

    FlightDto  expectedFlightDto  =

            createTestFlight(  outboundAirport,  inboundAirport);

    //  Exercise  System

    List  flightsAtOrigin  =

            facade.getFlightsByOriginAirport(outboundAirport);

    //  Verify  Outcome

    assertOnly1FlightInDtoList(  "Flights  at  origin",

                                            expectedFlightDto,

                                            flightsAtOrigin);

}

 

所有工作都在这里完成!创建方法已被修改以注册它刚刚创建的对象。

Here is where all the work gets done! The Creation Method has been modified to register the object it just created.

私有列表 allAirportIds;

私有列表 allFlights;



受保护的 void setUp()抛出异常 {

    allAirportIds = new ArrayList();

    allFlights = new ArrayList();

}

私有 BigDecimal createTestAirport(String airportName)

抛出 FlightBookingException {

    BigDecimal newAirportId = Facade.createAirport(

                               airportName,“Airport”+ airportName,

                               “City”+ airportName);

    allAirportIds.add(newAirportId);

    返回 newAirportId;

}

private  List  allAirportIds;

private  List  allFlights;



protected  void  setUp()  throws  Exception  {

    allAirportIds  =  new  ArrayList();

    allFlights  =  new  ArrayList();

}

private  BigDecimal  createTestAirport(String  airportName)

throws  FlightBookingException  {

    BigDecimal  newAirportId  =  facade.createAirport(

                               airportName,  "  Airport"  +  airportName,

                               "City"  +  airportName);

    allAirportIds.add(newAirportId);

    return  newAirportId;

}

 

接下来是实际的自动拆卸逻辑。在此示例中,它位于我们的Testcase 类中,并从方法中调用tearDown。为了使此示例非常简单,此逻辑专门用于处理机场和航班。更典型的是,它将位于Testcase 超类中,所有Testcase 类都可以使用它,并且将使用通用对象销毁机制,这样它就不必关心它正在删除的对象类型。

Next comes the actual Automated Teardown logic. In this example, it lives on our Testcase Class and is called from the tearDown method. To keep this example very simple, this logic has been written specifically to handle airports and flights. More typically, it would live in the Testcase Superclass, where it could be used by all Testcase Classes, and would use a generic object destruction mechanism so that it would not have to care what types of objects it was deleting.

protected void teadown() throws Exception {

     removeObjects(allAirportIds, "Airport");

     removeObjects(allFlights, "Flight");

}

public void removeObjects(ListobjectsToDelete, String type) {

     Iterator i =objectsToDelete.iterator();

     while (i.hasNext()) {

          try {

              BigDecimal id = (BigDecimal) i.next();

              if ("Airport"==type) {

                  Facade.removeAirport(id);

              } else {

                 Facade.removeFlight(id);

              }

          } catch (Exception e) {

                // 如果删除失败,则不执行任何操作

          }

     }

}

protected  void  tearDown()  throws  Exception  {

     removeObjects(allAirportIds,  "Airport");

     removeObjects(allFlights,  "Flight");

}

public  void  removeObjects(List  objectsToDelete,  String  type)  {

     Iterator  i  =  objectsToDelete.iterator();

     while  (i.hasNext())  {

          try  {

              BigDecimal  id  =  (BigDecimal)  i.next();

              if  ("Airport"==type)  {

                  facade.removeAirport(id);

              }  else  {

                 facade.removeFlight(id);

              }

          }  catch  (Exception  e)  {

                //  do  nothing  if  the  remove  failed

          }

     }

}

 

如果我们正在拆除共享装置tearDown,我们将用合适的注释或属性(例如@afterClass或)注释我们的方法[TestFixtureTearDown],或将其移动到安装装饰器

If we were tearing down a Shared Fixture, we would annotate our tearDown method with the suitable annotation or attribute (e.g., @afterClass or [TestFixtureTearDown]) or move it to a Setup Decorator.

示例:自动分解练习

Example: Automated Exercise Teardown

如果我们想采取下一步行动并自动拆除在 SUT 中创建的任何对象,我们可以修改 SUT 以使用可观察的对象工厂。在我们的测试中,我们将添加以下代码:

If we wanted to take the next step and automatically tear down any objects created within the SUT, we could modify the SUT to use an observable Object Factory. In our test, we would add the following code:

ResourceTracker 跟踪器;

public void setUp() {

     tracker = new ResourceTracker();

     ObjectFactory.addObserver(tracker);

}

public void teaDown() {

     tracker.cleanup();

     ObjectFactory.removeObserver(tracker);

}

ResourceTracker  tracker;

public  void  setUp()  {

     tracker  =  new  ResourceTracker();

     ObjectFactory.addObserver(tracker);

}

public  void  tearDown()  {

     tracker.cleanup();

     ObjectFactory.removeObserver(tracker);

}

 

最后一个例子假设自动拆卸逻辑已经被移到cleanup方法中ResourceTracker

This last example assumes that the Automated Teardown logic has been moved into the cleanup method on the ResourceTracker.

在线拆卸

In-line Teardown

我们如何拆卸测试夹具?

How do we tear down the Test Fixture?

我们在结果验证之后立即在测试方法的末尾加入了拆卸逻辑

We include teardown logic at the end of the Test Method immediately after the result verification.

图像

要使测试可重复且稳健,很大一部分工作就是确保每次测试后拆除测试装置,并为下一次测试运行创建一个新的装置。此策略称为Fresh Fixture第 311页)。剩余的对象和数据库记录以及打开的文件和连接,在最好的情况下会导致性能下降,在最坏的情况下会导致测试失败或系统崩溃。虽然其中一些资源可能会通过垃圾回收自动清理,但如果没有明确拆除,其他资源可能会被搁置。

A large part of making tests repeatable and robust is ensuring that the test fixture is torn down after each test and a new one created for the next test run. This strategy is known as a Fresh Fixture (page 311). Leftover objects and database records, as well as open files and connections, can at best cause performance degradations and at worst cause tests to fail or systems to crash. While some of these resources may be cleaned up automatically by garbage collection, others may be left hanging if they are not torn down explicitly.

至少,我们应该编写内联拆卸代码来清理测试后剩余的资源。

At a minimum, we should write In-line Teardown code that cleans up resources left over after our test.

工作原理

How It Works

在编写测试时,我们会在心里记住测试创建的所有不会被自动清理的对象。在编写代码来执行 SUT 并验证结果后,我们会在测试方法(第 348页) 的末尾添加逻辑,以销毁垃圾收集器不会自动清理的任何对象。我们使用相关的语言功能来确保无论测试结果如何,拆卸代码都会运行。

As we write a test, we mentally keep track of all objects the test creates that will not be cleaned up automatically. After writing the code to exercise the SUT and verify the outcome, we add logic to the end of the Test Method (page 348) to destroy any objects that will not be cleaned up automatically by the garbage collector. We use the relevant language feature to ensure that the teardown code is run regardless of the outcome of the test.

何时使用它

When to Use It

每当我们有资源在测试方法运行后不会自动释放时,我们就应该使用某种形式的拆卸逻辑;当每个测试都有不同的对象需要清理时,我们可以使用内联拆卸。我们可能会发现需要清理的对象,因为我们有不可重复的测试(请参阅第228页的不稳定测试)或缓慢的测试(第253页),这是由于多次测试运行中积累的碎片造成的。

We should use some form of teardown logic whenever we have resources that will not be freed automatically after the Test Method is run; we can use In-line Teardown when each test has different objects to clean up. We may discover that objects need to be cleaned up because we have Unrepeatable Tests (see Erratic Test on page 228) or Slow Tests (page 253) caused by the accumulation of detritus from many test runs.

与夹具设置不同,从测试即文档(参见第 23页)的角度来看,拆卸逻辑并不重要。使用任何形式的拆卸逻辑都可能导致高测试维护成本(第265页),应尽可能避免。因此,以内联方式包含拆卸逻辑的唯一真正好处是它可能使维护拆卸逻辑变得更容易 - 事实上,好处很小。如果我们使用每个夹具的测试用例类(第 631页),其中测试用例类(第373页)中的所有测试都有相同的测试夹具,那么努力实现自动拆卸(第 503页)或使用隐式拆卸(第516页)几乎总是更好的。

Unlike fixture setup, the teardown logic is not important from the perspective of Tests as Documentation (see page 23). Use of any form of teardown logic may potentially contribute to High Test Maintenance Cost (page 265) and should be avoided if at all possible. Thus the only real benefit of including the teardown logic on an in-line basis is that it may make it easier to maintain the teardown logic—a pretty slim benefit, indeed. It is almost always better to strive for Automated Teardown (page 503) or to use Implicit Teardown (page 516) if we are using Testcase Class per Fixture (page 631), where all tests in a Testcase Class (page 373) have the same test fixture.

我们还可以使用内联拆卸作为隐式拆卸的垫脚石从而遵循“尽可能最简单的方法”的原则。首先,我们学习如何对每个测试方法进行内联拆卸;接下来,我们将这些测试中的通用逻辑提取到方法中。如果测试创建的对象受自动内存管理的影响,我们应该使用内联拆卸。在这种情况下,我们应该使用垃圾收集拆卸第 500页),因为它更不容易出错,并使测试更易于理解和维护。tearDown

We can also use In-line Teardown as a steppingstone to Implicit Teardown, thereby following the principle of "the simplest thing that could possibly work." First, we learn how to do In-line Teardown for each Test Method; next, we extract the common logic from those tests into the tearDown method. We should not use In-line Teardown if the objects created by the test are subject to automated memory management. In such a case, we should use Garbage-Collected Teardown (page 500) instead because it is much less error-prone and keeps the tests easier to understand and maintain.

实施说明

Implementation Notes

内联拆卸中的主要考虑是确保拆卸代码确实运行,即使测试因断言方法(第 362页) 而失败,或以 SUT 或测试方法中的错误结束。次要考虑是确保拆卸代码不会引入其他错误。

The primary consideration in In-line Teardown is ensuring that the teardown code actually runs even when the test is failed by an Assertion Method (page 362) or ends in an error in either the SUT or the Test Method. A secondary consideration is ensuring that the teardown code does not introduce additional errors.

正确执行内联拆卸的关键是使用语言级构造来确保拆卸代码运行。大多数现代语言都包含某种错误/异常处理构造,它将尝试执行一个代码块,并保证无论第一个代码块如何终止,第二个代码块都会运行。在 Java 中,此构造采用try带有关联finally块的块的形式。

The key to doing In–line Teardown correctly is to use language–level constructs to ensure that the teardown code is run. Most modern languages include some sort of error/exception–handling construct that will attempt the execution of a block of code with the guarantee that a second block of code will be run regardless of how the first block terminates. In Java, this construct takes the form of a try block with an associated finally block.

变体:拆除保护条款

为了防止因尝试拆除不存在的资源而导致的故障,我们可以在逻辑周围放置一个“保护条款”。它的加入降低了拆除逻辑导致测试错误的可能性。

To protect against a failure caused by trying to tear down a resource that doesn't exist, we can put a "guard clause" around the logic. Its inclusion reduces the likelihood of a test error caused by the teardown logic.

变体:委托拆卸

我们可以通过调用测试实用程序方法第 599页)将大部分拆卸逻辑移出测试方法。虽然此策略减少了使测试混乱的拆卸逻辑数量,但我们仍然需要至少在断言和 SUT 的执行周围放置一个错误处理构造,以确保它被调用。使用隐式拆卸几乎总是更好的解决方案。

We can move much of the teardown logic out of the Test Method by calling a Test Utility Method (page 599). Although this strategy reduces the amount of teardown logic cluttering the test, we still need to place an error-handling construct around at least the assertions and the exercising of the SUT to ensure that it gets called. Using Implicit Teardown is almost always a better solution.

变体:简单的在线拆卸

当我们忘记在测试逻辑周围放置相当于try/finally块的东西以确保拆卸逻辑始终执行时,就会出现简单的内联拆卸。这会导致资源泄漏(参见不稳定的测试),进而可能导致不稳定的测试

Naive In-line Teardown is what we have when we forget to put the equivalent of a try/finally block around our test logic to ensure that our teardown logic always executes. It leads to Resource Leakage (see Erratic Test), which in turn may lead to Erratic Tests.

激励人心的例子

Motivating Example

以下测试创建一个持久对象 ( airport) 作为基座的一部分。由于该对象存储在数据库中,因此它不受垃圾收集拆卸的影响,必须明确销毁。如果我们不在测试中包含拆卸逻辑,则每次运行测试时,它都会在数据库中创建另一个对象。这可能会导致不可重复的测试,除非测试使用不同的生成值(请参阅第723页的生成值)来确保创建的对象不违反任何唯一键约束。

The following test creates a persistent object (airport) as part of the fixture. Because the object is stored in a database, it is not subject to Garbage-Collected Teardown and must be explicitly destroyed. If we do not include teardown logic in the test, each time the test is run it will create another object in the database. This may lead to Unrepeatable Tests unless the test uses Distinct Generated Values (see Generated Value on page 723) to ensure that the created objects do not violate any unique key constraints.

public void testGetFlightsByOriginAirport_NoFlights_ntd()

             throws Exception {

     // Fixture Setup

     BigDecimal outboundAirport = createTestAirport("1OF");

     // 练习系统

     列表 flightsAtDestination1 =

             Facade.getFlightsByOriginAirport(outboundAirport);

     // 验证结果

     assertEquals(0,flightsAtDestination1.size());

}

public  void  testGetFlightsByOriginAirport_NoFlights_ntd()

             throws  Exception  {

     //  Fixture  Setup

     BigDecimal  outboundAirport  =  createTestAirport("1OF");

     //  Exercise  System

     List  flightsAtDestination1  =

             facade.getFlightsByOriginAirport(outboundAirport);

     //  Verify  Outcome

     assertEquals(0,flightsAtDestination1.size());

}

 

示例:简单的内联拆卸

Example: Naive In-line Teardown

在这个问题的简单解决方案中,我们在断言后添加了一行来销毁airport在夹具设置中创建的内容。

In this naive solution to this problem, we added a line after the assertion to destroy the airport created in the fixture setup.

public void testGetFlightsByOriginAirport_NoFlights()

             throws Exception {

     // Fixture Setup

     BigDecimal outboundAirport = createTestAirport("1OF");

     // 练习系统

     列表 flightsAtDestination1 =

             Facade.getFlightsByOriginAirport(outboundAirport);

     // 验证结果

     assertEquals(0,flightsAtDestination1.size());

     Facade.removeAirport(outboundAirport);

}

public  void  testGetFlightsByOriginAirport_NoFlights()

             throws  Exception  {

     //  Fixture  Setup

     BigDecimal  outboundAirport  =  createTestAirport("1OF");

     //  Exercise  System

     List  flightsAtDestination1  =

             facade.getFlightsByOriginAirport(outboundAirport);

     //  Verify  Outcome

     assertEquals(0,flightsAtDestination1.size());

     facade.removeAirport(outboundAirport);

}

 

不幸的是,这个解决方案并不适用,因为如果 SUT 遇到错误或断言失败,拆卸逻辑将不会被执行。我们可以尝试将夹具清理移到断言之前,但这仍然无法解决 SUT 内部发生错误的问题。显然,我们需要一个更通用的解决方案。

Unfortunately, this solution isn't really adequate because the teardown logic won't be exercised if the SUT encounters an error or if the assertions fail. We could try moving the fixture cleanup before the assertions but this still wouldn't address the issue with errors occurring inside the SUT. Clearly, we need a more general solution.

重构说明

Refactoring Notes

我们要么需要在 SUT 和断言的执行周围放置一个错误处理结构,要么将拆卸代码移到tearDown方法中。无论哪种方式,我们都需要确保所有try/finally拆卸代码都能运行,即使其中某些部分失败。这通常涉及在拆卸过程的每个步骤周围明智地使用控制结构。

We need either to place an error-handling construct around the exercising of the SUT and the assertions or to move the teardown code into the tearDown method. Either way, we need to ensure that all the teardown code runs, even if some parts of it fail. This usually involves the judicious use of try/finally control structures around each step of the teardown process.

示例:在线拆卸

Example: In-line Teardown

try/finally在这个 Java 示例中,我们围绕测试的练习 SUT 和结果验证阶段引入了一个块,以确保我们的拆卸代码能够运行。

In this Java example, we have introduced a try/finally block around the exercise SUT and result verification phases of the test to ensure that our teardown code is run.

public void testGetFlightsByOriginAirport_NoFlights_td()

             throws Exception {

     // Fixture Setup

     BigDecimal outboundAirport = createTestAirport("1OF");

     try {

          // Exercise System

          List flightsAtDestination1 =

                   Facade.getFlightsByOriginAirport(outboundAirport);

          // 验证结果

          assertEquals(0,flightsAtDestination1.size());

          } finally {

               facade.removeAirport(outboundAirport);

           }

}

public  void  testGetFlightsByOriginAirport_NoFlights_td()

             throws  Exception  {

     //  Fixture  Setup

     BigDecimal  outboundAirport  =  createTestAirport("1OF");

     try  {

          //  Exercise  System

          List  flightsAtDestination1  =

                   facade.getFlightsByOriginAirport(outboundAirport);

          //  Verify  Outcome

          assertEquals(0,flightsAtDestination1.size());

          }  finally  {

               facade.removeAirport(outboundAirport);

           }

}

 

现在,SUT 的执行和断言都出现在try块中,并且拆卸逻辑也出现在块中。这种分离对于使内联拆卸finally正常工作至关重要。除非我们正在编写预期异常测试(请参阅测试方法),否则我们应该包含块。catch

Now the exercising of the SUT and the assertions both appear in the try block and the teardown logic is found in the finally block. This separation is crucial to making In-line Teardown work properly. We should not include a catch block unless we are writing an Expected Exception Test (see Test Method).

示例:拆卸保护条款

Example: Teardown Guard Clause

在这里,我们在拆卸代码中添加了拆卸保护子句,airport以确保如果不存在则不会运行它:

Here, we've added a Teardown Guard Clause to the teardown code to ensure it isn't run if the airport doesn't exist:

public void testGetFlightsByOriginAirport_NoFlights_TDGC()

              throws Exception {

    // Fixture Setup

    BigDecimal outboundAirport = createTestAirport("1OF");

    try {

          // Exercise System

          List flightsAtDestination1 =

                    Facade.getFlightsByOriginAirport(outboundAirport);

          // 验证结果

          assertEquals(0,flightsAtDestination1.size());

    } finally {

          if (outboundAirport!=null) {

              Facade.removeAirport(outboundAirport);

          }

    }

}

public  void  testGetFlightsByOriginAirport_NoFlights_TDGC()

              throws  Exception  {

    //  Fixture  Setup

    BigDecimal  outboundAirport  =  createTestAirport("1OF");

    try  {

          //  Exercise  System

          List  flightsAtDestination1  =

                    facade.getFlightsByOriginAirport(outboundAirport);

          //  Verify  Outcome

          assertEquals(0,flightsAtDestination1.size());

    }  finally  {

          if  (outboundAirport!=null)  {

              facade.removeAirport(outboundAirport);

          }

    }

}

 

示例:多资源内联拆卸(Java)

Example: Multiresource In-line Teardown (Java)

如果需要在同一个测试中清理多个资源,我们必须确保所有拆卸代码都能运行,即使某些拆卸语句包含错误。可以通过将每个后续拆卸步骤嵌套在另一个保证代码块中来实现此目标,如以下 Java 示例所示:

If multiple resources need to be cleaned up in the same test, we must ensure that all the teardown code runs even if some of the teardown statements contain errors. This goal can be accomplished by nesting each subsequent teardown step inside another block of guaranteed code, as in this Java example:

public void testGetFlightsByOrigin_NoInboundFlight_SMRTD()

              throws Exception {

      // Fixture Setup

      BigDecimal outboundAirport = createTestAirport("1OF");

      BigDecimal inboundAirport = null;

      FlightDto expFlightDto = null;

      try {

          inboundAirport = createTestAirport("1IF");

          expFlightDto = createTestFlight(outboundAirport, inboundAirport);

        // 练习系统

        列表 flightsAtDestination1 =

                Facade.getFlightsByOriginAirport(inboundAirport);

        // 验证结果

        assertEquals(0,flightsAtDestination1.size());

} finally {

      try {

            Facade.removeFlight(expFlightDto.getFlightNumber());

      } finally {

            try {

                Facade.removeAirport(inboundAirport);

            } finally {

                Facade.removeAirport(outboundAirport);

            }

        }

    }

}

public  void  testGetFlightsByOrigin_NoInboundFlight_SMRTD()

              throws  Exception  {

      //  Fixture  Setup

      BigDecimal  outboundAirport  =  createTestAirport("1OF");

      BigDecimal  inboundAirport  =  null;

      FlightDto  expFlightDto  =  null;

      try  {

          inboundAirport  =  createTestAirport("1IF");

          expFlightDto  =  createTestFlight(outboundAirport,  inboundAirport);

        //  Exercise  System

        List  flightsAtDestination1  =

                facade.getFlightsByOriginAirport(inboundAirport);

        //  Verify  Outcome

        assertEquals(0,flightsAtDestination1.size());

}  finally  {

      try  {

            facade.removeFlight(expFlightDto.getFlightNumber());

      }  finally  {

            try  {

                facade.removeAirport(inboundAirport);

            }  finally    {

                facade.removeAirport(outboundAirport);

            }

        }

    }

}

 

如果我们必须清理多个资源,这种方案很快就会变得非常混乱。在这种情况下,将资源组织成一个数组或列表,然后迭代该数组或列表更有意义。此时,我们已经实现了自动拆卸的一半了。

This scheme gets very messy in a hurry if we must clean up more than a few resources. In such a situation, it makes more sense to organize the resources into an array or list and then to iterate over that array or list. At that point we are halfway to implementing Automated Teardown.

示例:委托拆卸

Example: Delegated Teardown

如果我们不相信我们可以提出一种适用于所有测试的完全通用的清理策略,我们还可以从测试方法内部委托拆卸。

We can also delegate the teardown from within the Test Method if we don't believe we can come up with a completely generic way cleanup strategy that will work for all tests.

public void testGetFlightsByOrigin_NoInboundFlight_DTD()

              throws Exception {

      // Fixture Setup

      BigDecimal outboundAirport = createTestAirport("1OF");

      BigDecimal inboundAirport = null;

      FlightDto expectedFlightDto = null;

      try {

          inboundAirport = createTestAirport("1IF");

          expectedFlightDto =

                createTestFlight( outboundAirport, inboundAirport);

            // 练习系统

            列表 flightsAtDestination1 =

                Facade.getFlightsByOriginAirport(inboundAirport);

            // 验证结果

            assertEquals(0,flightsAtDestination1.size());

    } finally {

          trashdownFlightAndAirports( outboundAirport,

                                                      inboundAirport,

                                                    expectedFlightDto);

    }

}

private void trashdownFlightAndAirports(

                                                         BigDecimal firstAirport,

                                                         BigDecimal secondAirport,

                                                         FlightDto flightDto)

                throws FlightBookingException {

    try {

          Facade.removeFlight( flightDto.getFlightNumber() );

  } finally {

       try {

            Facade.removeAirport(secondAirport);

      } finally {

            Facade.removeAirport(firstAirport);

        }

  }

}

public  void  testGetFlightsByOrigin_NoInboundFlight_DTD()

              throws  Exception  {

      //  Fixture  Setup

      BigDecimal  outboundAirport  =  createTestAirport("1OF");

      BigDecimal  inboundAirport  =  null;

      FlightDto  expectedFlightDto  =  null;

      try  {

          inboundAirport  =  createTestAirport("1IF");

          expectedFlightDto  =

                createTestFlight(  outboundAirport,  inboundAirport);

            //  Exercise  System

            List  flightsAtDestination1  =

                facade.getFlightsByOriginAirport(inboundAirport);

            //  Verify  Outcome

            assertEquals(0,flightsAtDestination1.size());

    }  finally  {

          teardownFlightAndAirports(  outboundAirport,

                                                      inboundAirport,

                                                    expectedFlightDto);

    }

}

private  void  teardownFlightAndAirports(

                                                         BigDecimal  firstAirport,

                                                         BigDecimal  secondAirport,

                                                         FlightDto  flightDto)

                throws  FlightBookingException  {

    try  {

          facade.removeFlight(  flightDto.getFlightNumber()  );

  }  finally  {

       try  {

            facade.removeAirport(secondAirport);

      }  finally    {

            facade.removeAirport(firstAirport);

        }

  }

}

 

我们之中的优化人员会注意到,这两个航班号实际上可作为 的属性使用flightDto。偏执狂会反驳说,因为teardownFlightAndAirports可以在 构造 之前调用该方法flightDto,所以我们不能指望使用它来访问Airports。因此,我们必须Airports单独传递 。需要这样思考,这就是通用自动拆卸如此有吸引力的原因;它完全避免了思考!

The optimizers among us will notice that the two flight numbers are actually available as attributes of the flightDto. The paranoid will counter that because the teardownFlightAndAirports method could be called before the flightDto is constructed, we cannot count on using it to access the Airports. Hence we must pass the Airports in individually. The need to think this way is why a generic Automated Teardown is so attractive; it avoids having to think at all!

隐式拆卸

Implicit Teardown

也称为

Also known as

挂钩拆卸、框架调用拆卸、拆卸方法

Hooked Teardown, Framework-Invoked Teardown, Teardown Method

我们如何拆卸测试夹具?

How do we tear down the Test Fixture?

测试自动化框架在tearDown每个测试方法之后的方法中调用我们的清理逻辑。

The Test Automation Framework calls our cleanup logic in the tearDown method after every Test Method.

图像

使测试可重复且稳健的很大一部分是确保在每次测试后拆除测试装置,并为下一次测试运行创建一个新的装置。此策略称为Fresh Fixture第 311页)。剩余的对象和数据库记录以及打开的文件和连接,在最好的情况下会导致性能下降,在最坏的情况下会导致测试失败或系统崩溃。

A large part of making tests repeatable and robust is ensuring that the test fixture is torn down after each test and a new one created for the next test run. This strategy is known as a Fresh Fixture (page 311). Leftover objects and database records, as well as open files and connections, can at best cause performance degradations and at worst cause tests to fail or systems to crash.

当我们无法利用垃圾收集拆卸(第 500页) 并且我们有几个需要拆卸相同对象的测试时,我们可以将拆卸逻辑放入一个特殊tearDown方法中,该方法由测试自动化框架(第 298页) 在运行每个测试方法(第 348页) 后调用。

When we can't take advantage of Garbage-Collected Teardown (page 500) and we have several tests with the same objects to tear down, we can put the teardown logic into a special tearDown method that the Test Automation Framework (page 298) calls after each Test Method (page 348) is run.

工作原理

How It Works

任何需要清理的内容都可以在四阶段测试(第 358页) 的最后阶段(即夹具拆卸阶段)释放或销毁。xUnit 系列测试自动化框架的大多数成员都支持隐式拆卸的概念,即在测试方法运行后调用tearDown每个测试用例对象(第 382页)的方法。

Anything that needs to be cleaned up can be freed or destroyed in the final phase of the Four-Phase Test (page 358)—namely, the fixture teardown phase. Most members of the xUnit family of Test Automation Frameworks support the concept of Implicit Teardown, wherein they call the tearDown method of each Testcase Object (page 382) after the Test Method has been run.

tearDown无论测试通过与否,都会调用该方法。此方案确保我们有机会进行清理,不受任何失败断言的干扰。但请注意,tearDown如果该setUp方法引发错误,xUnit 系列的许多成员都不会调用。

The tearDown method is called regardless of whether the test passes or fails. This scheme ensures that we have the opportunity to clean up, undisturbed by any failed assertions. Be aware, however, that many members of the xUnit family do not call tearDown if the setUp method raises an error.

何时使用它

When to Use It

当测试完成后需要显式销毁或释放具有相同资源的多个测试,并且这些资源不会自动销毁或释放时,我们可以使用隐式拆卸。我们可能会发现这一要求,因为我们有不可重复的测试(请参阅第 228页的不稳定测试)或缓慢的测试(第253页),这是由于多次测试运行中积累的碎片造成的。

We can use Implicit Teardown whenever several tests with the same resources need to be destroyed or freed explicitly after the test has been completed and those resources will not be destroyed or freed automatically. We may discover this requirement because we have Unrepeatable Tests (see Erratic Test on page 228) or Slow Tests (page 253) caused by the accumulation of detritus from many test runs.

如果测试创建的对象是内部资源,并且受自动内存管理的约束,那么垃圾收集拆卸可能会为我们省去很多工作。如果每个测试都有一组完全不同的对象需要拆卸,那么内联拆卸第 509页)可能更合适。在许多情况下,我们可以通过使用自动拆卸第 503页)完全避免手动编写拆卸逻辑。

If the objects created by the test are internal resources and subject to automated memory management, then Garbage-Collected Teardown may eliminate a lot of work for us. If each test has a completely different set of objects to tear down, then In-line Teardown (page 509) may be more appropriate. In many cases, we can completely avoid manually written teardown logic by using Automated Teardown (page 503).

实施说明

Implementation Notes

方法中的拆卸逻辑tearDown通常是通过重构具有内联拆卸的测试创建的。tearDown出于以下几个原因,该方法可能需要“灵活”或“适应性”:

The teardown logic in the tearDown method is most often created by refactoring from tests that had In-line Teardown. The tearDown method may need to be "flexible" or "accommodating" for several reasons:

  • 当测试失败或者发生测试错误时,测试方法可能没有创建所有的夹具对象。
  • When a test fails or when a test error occurs, the Test Method may not have created all the fixture objects.
  • 如果测试用例类(第 373页)中的所有测试方法不使用相同的装置,可能需要为不同的测试清理不同的对象集。
  • If all the Test Methods in the Testcase Class (page 373) don't use identical fixtures,1 there may be different sets of objects to clean up for different tests.
变体:拆除保护条款

如果我们处理只有一部分要拆除的对象实际存在的情况,可以避免任意条件测试逻辑第 200if页),方法是在每个拆除操作周围放置一个保护子句(一个简单的语句)来防止资源不存在。使用这种技术,适当编码的tearDown方法可以拆除各种装置配置。与此形成对比的是,该方法只能为共享它的测试方法setUp设置最低公分母装置。

We can avoid arbitrarily Conditional Test Logic (page 200) if we deal with the case where only a subset of the objects to be torn down are actually present by putting a guard clause (a simple if statement) around each teardown operation to guard against the resource not being present. With this technique, a suitably coded tearDown method can tear down various fixture configurations. Contrast this with the setUp method, which can set up only the lowest common denominator fixture for the Test Methods that share it.

激励人心的例子

Motivating Example

以下测试在装置设置期间创建了几个标准对象。由于这些对象会持久保存在数据库中,因此每次测试后都必须明确清理它们。每个测试(此处仅显示其中一个)都包含相同的内联拆卸逻辑来删除这些对象。

The following test creates several standard objects during fixture setup. Because the objects are persisted in a database, they must be cleaned up explicitly after every test. Each test (only one of several is shown here) contains the same in-line teardown logic to delete the objects.

public void testGetFlightsByOrigin_NoInboundFlight_SMRTD()

                throws Exception {

      // Fixture Setup

      BigDecimal outboundAirport = createTestAirport("1OF");

      BigDecimal inboundAirport = null;

      FlightDto expFlightDto = null;

      try {

          inboundAirport = createTestAirport("1IF");

          expFlightDto = createTestFlight(outboundAirport, inboundAirport);

          // 练习系统

          列表 flightsAtDestination1 =

                   Facade.getFlightsByOriginAirport(inboundAirport);

          // 验证结果

          assertEquals(0,flightsAtDestination1.size());

    } finally {

         try {

             Facade.removeFlight(expFlightDto.getFlightNumber());

        } finally {

              try {

                  Facade.removeAirport(inboundAirport);

              } finally {

                  Facade.removeAirport(outboundAirport);

              }

        }

  }

}

public  void  testGetFlightsByOrigin_NoInboundFlight_SMRTD()

                throws  Exception  {

      //  Fixture  Setup

      BigDecimal  outboundAirport  =  createTestAirport("1OF");

      BigDecimal  inboundAirport  =  null;

      FlightDto  expFlightDto  =  null;

      try  {

          inboundAirport  =  createTestAirport("1IF");

          expFlightDto  =  createTestFlight(outboundAirport,  inboundAirport);

          //  Exercise  System

          List  flightsAtDestination1  =

                   facade.getFlightsByOriginAirport(inboundAirport);

          //  Verify  Outcome

          assertEquals(0,flightsAtDestination1.size());

    }  finally  {

         try  {

             facade.removeFlight(expFlightDto.getFlightNumber());

        }  finally  {

              try  {

                  facade.removeAirport(inboundAirport);

              }  finally    {

                  facade.removeAirport(outboundAirport);

              }

        }

  }

}

 

这里有足够多的测试代码重复第 213页),足以保证将这些测试转换为隐式拆卸

There is enough Test Code Duplication (page 213) here to warrant converting these tests to Implicit Teardown.

重构说明

Refactoring Notes

首先,我们在所有测试中找到最具代表性的拆卸示例。接下来,我们对该代码进行提取方法 [Fowler] 重构并调用生成的方法tearDown。最后,我们删除其他每个测试中的拆卸逻辑。我们可能需要在任何可能不是每个测试都需要的拆卸逻辑周围引入拆卸保护条款。我们还应该用一个块包围每个拆卸尝试,try/finally以确保即使之前的尝试失败,剩余的拆卸逻辑也会执行。

First, we find the most representative example of teardown in all the tests. Next, we do an Extract Method [Fowler] refactoring on that code and call the resulting method tearDown. Finally, we delete the teardown logic in each of the other tests. We may need to introduce Teardown Guard Clauses around any teardown logic that may not be needed in every test. We should also surround each teardown attempt with a try/finally block to ensure that the remaining teardown logic executes even if an earlier attempt fails.

示例:隐式拆卸

Example: Implicit Teardown

此示例展示了相同的测试,但将拆卸逻辑从tearDown方法中删除。请注意测试变得有多小。

This example shows the same tests with the teardown logic removed to the tearDown method. Note how much smaller the tests have become.

BigDecimal outboundAirport;

BigDecimal inboundAirport;

FlightDto expFlightDto;



public void testGetFlightsByAirport_NoInboundFlights_NIT()

              throws Exception {

     // Fixture Setup

     outboundAirport = createTestAirport("1OF");

     inboundAirport = createTestAirport("1IF");

     expFlightDto = createTestFlight( outboundAirport, inboundAirport);

     // 练习系统

     列表 flightsAtDestination1 =

              Facade.getFlightsByOriginAirport(inboundAirport);

     // 验证结果

     assertEquals(0,flightsAtDestination1.size());

}



protected void teaDown() throws Exception {

    try {

         Facade.removeFlight( expFlightDto.getFlightNumber() );

    } finally {

         try {

             Facade.removeAirport(inboundAirport);

         } finally {

             Facade.removeAirport(outboundAirport);

          }

    }

}

BigDecimal  outboundAirport;

BigDecimal  inboundAirport;

FlightDto  expFlightDto;



public  void  testGetFlightsByAirport_NoInboundFlights_NIT()

              throws  Exception  {

     //  Fixture  Setup

     outboundAirport  =  createTestAirport("1OF");

     inboundAirport  =  createTestAirport("1IF");

     expFlightDto  =  createTestFlight(  outboundAirport,  inboundAirport);

     //  Exercise  System

     List  flightsAtDestination1  =

              facade.getFlightsByOriginAirport(inboundAirport);

     //  Verify  Outcome

     assertEquals(0,flightsAtDestination1.size());

}



protected  void  tearDown()  throws  Exception  {

    try  {

         facade.removeFlight(  expFlightDto.getFlightNumber()  );

    }  finally  {

         try  {

             facade.removeAirport(inboundAirport);

         }  finally    {

             facade.removeAirport(outboundAirport);

          }

    }

}

 

try/finally请注意,在执行 SUT 和断言时没有使用任何块。此结构有助于测试读者理解这不是预期异常测试(请参阅测试方法)。此外,我们不需要在每个操作前面放置一个 Guard Clause [SBPP]try/finally ,因为该块可确保故障不会造成灾难性后果;因此尝试执行该操作不会造成任何实际危害。我们确实必须将保存局部变量的装置转换为实例变量,以允许该tearDown方法访问装置。

Note that there is no try/finally block around the exercising of the SUT and the assertions. This structure helps the test reader understand that this is not an Expected Exception Test (see Test Method). Also, we didn't need to put a Guard Clause [SBPP] in front of each operation because the try/finally block ensures that a failure is noncatastrophic; thus there is no real harm in trying to perform the operation. We did have to convert our fixture holding local variables into instance variables to allow the tearDown method to access the fixture.

第 23 章

测试双重模式

Chapter 23

Test Double Patterns

 

本章中的模式

Patterns in This Chapter

测试替身 522

Test Double 522

测试替身用法

Test Double Usage

      

测试桩 529

      

Test Stub 529

      

测试间谍 538

      

Test Spy 538

      

模拟对象 544

      

Mock Object 544

      

伪造对象 551

      

Fake Object 551

测试替身构建

Test Double Construction

      

可配置测试替身 558

      

Configurable Test Double 558

      

硬编码测试替身 568

      

Hard-Coded Test Double 568

测试特定子类 579

Test-Specific Subclass 579

测试替身

Test Double

也称为

Also known as

冒名顶替者

Imposter

当依赖的代码无法使用时,我们如何独立验证逻辑?

如何避免测试缓慢?

How can we verify logic independently when code it depends on is unusable?

How can we avoid Slow Tests?

我们用“测试特定的等效物”替换 SUT 所依赖的组件。

We replace a component on which the SUT depends with a "test-specific equivalent."

图像

有时,测试 SUT 非常困难,因为它依赖于测试环境中无法使用的其他组件。这种情况可能是因为这些组件不可用,因为它们不会返回测试所需的结果,或者因为执行它们会产生不良的副作用。在其他情况下,我们的测试策略要求我们对 SUT 的内部行为有更好的控制或可见性。

Sometimes it is just plain hard to test the SUT because it depends on other components that cannot be used in the test environment. Such a situation may arise because those components aren't available, because they will not return the results needed for the test, or because executing them would have undesirable side effects. In other cases, our test strategy requires us to have more control over or visibility of the internal behavior of the SUT.

当我们编写测试时,如果不能(或选择不使用)使用真实依赖组件 (DOC),我们可以用测试替身 (Test Double)替换它。测试替身不必完全像真实 DOC 那样运作;它只需提供与真实 DOC 相同的 API,这样 SUT就会认为它是真实的!

When we are writing a test in which we cannot (or choose not to) use a real depended-on component (DOC), we can replace it with a Test Double. The Test Double doesn't have to behave exactly like the real DOC; it merely has to provide the same API as the real DOC so that the SUT thinks it is the real one!

工作原理

How It Works

当电影制片人想要拍摄一些对主演来说有潜在风险或危险的场景时,他们会聘请“替身演员”来代替演员出演。替身演员是经过严格训练的人员,能够满足场景的特定要求。替身演员可能无法演戏,但他或她知道如何从高处坠落、撞车或做场景要求的任何事情。替身演员需要与演员的相似程度取决于场景的性质。通常,可以安排一个身材与演员略微相似的人来代替演员。

When the producers of a movie want to film something that is potentially risky or dangerous for the leading actor to carry out, they hire a "stunt double" to take the place of the actor in the scene. The stunt double is a highly trained individual who is capable of meeting the specific requirements of the scene. The stunt double may not be able to act, but he or she knows how to fall from great heights, crash a car, or do whatever the scene calls for. How closely the stunt double needs to resemble the actor depends on the nature of the scene. Usually, things can be arranged such that someone who vaguely resembles the actor in stature can take the actor's place.

为了测试目的,我们可以用相当于“替身”的测试替身来替换真正的 DOC(不是 SUT!) 。在四阶段测试第 358页)的夹具设置阶段,我们用测试替身替换真正的 DOC 。根据我们正在执行的测试类型,我们可以对测试替身行为进行硬编码,也可以在设置阶段对其进行配置。当 SUT 与测试替身交互时,它不会意识到它不是在与真正的人交谈,但我们已经实现了让不可能的测试成为可能的目标。

For testing purposes, we can replace the real DOC (not the SUT!) with our equivalent of the "stunt double": the Test Double. During the fixture setup phase of our Four-Phase Test (page 358), we replace the real DOC with our Test Double. Depending on the kind of test we are executing, we may hard-code the behavior of the Test Double or we may configure it during the setup phase. When the SUT interacts with the Test Double, it won't be aware that it isn't talking to the real McCoy, but we will have achieved our goal of making impossible tests possible.

无论我们选择使用哪种测试替身变体,我们都必须记住,我们不需要实现 DOC 的整个接口。相反,我们只提供特定测试所需的功能。我们甚至可以为涉及相同 DOC 的不同测试构建不同的测试替身。

Regardless of which variation of Test Double we choose to use, we must keep in mind that we don't need to implement the entire interface of the DOC. Instead, we provide only the functionality needed for our particular test. We can even build different Test Doubles for different tests that involve the same DOC.

何时使用它

When to Use It

在下列情况下,我们可能希望在测试中使用某种测试替身:

We might want to use some sort of Test Double during our tests in the following circumstances:

  • 如果我们有一个未经测试的需求(请参阅第 268页的生产错误),因为 SUT 及其 DOC 都没有为 SUT 的间接输出提供观察点,我们需要使用行为验证第 468页)来验证
  • If we have an Untested Requirement (see Production Bugs on page 268) because neither the SUT nor its DOCs provide an observation point for the SUT's indirect output that we need to verify using Behavior Verification (page 468)
  • 如果我们有未经测试的代码(参见生产错误),并且 DOC 没有提供控制点来允许我们使用必要的间接输入来运行 SUT
  • If we have Untested Code (see Production Bugs) and a DOC does not provide the control point to allow us to exercise the SUT with the necessary indirect inputs
  • 如果我们有慢速测试第 253页),并且我们希望能够更快地运行测试,从而更频繁地
  • If we have Slow Tests (page 253) and we want to be able to run our tests more quickly and hence more often

上述每种情况都可以通过使用测试替身来解决。当然,使用测试替身时必须小心,因为我们测试的 SUT 与生产中使用的配置不同。因此,我们确实应该至少进行一次测试,以验证 SUT 在没有测试替身的情况下是否能正常工作。我们需要小心,不要替换我们试图验证的 SUT 的部分,因为这种做法可能会导致测试错误的软件!此外,过度使用测试替身可能会导致由于过度指定软件而导致的脆弱测试第 239页) 。

Each of these scenarios can be addressed in some way by using a Test Double. Of course, we have to be careful when using Test Doubles because we are testing the SUT in a different configuration from the one that will be used in production. For this reason, we really should have at least one test that verifies the SUT works without a Test Double. We need to be careful that we don't replace the parts of the SUT that we are trying to verify because that practice can result in tests that test the wrong software! Also, excessive use of Test Doubles can result in Fragile Tests (page 239) as a result of Overspecified Software.

测试替身有几种主要类型,如图 23.1所示。这些模式的实现变体在相应的模式说明中有更详细的描述。

Test Doubles come in several major flavors, as summarized in Figure 23.1. The implementation variations of these patterns are described in more detail in the corresponding pattern write-ups.

图 23.1。 测试替身的类型。虚拟对象实际上是值模式的替代方案。测试桩用于验证间接输入;测试间谍和模拟对象用于验证间接输出。伪对象提供了另一种实现方式。

Figure 23.1. Types of Test Doubles. Dummy Objects are really an alternative to the value patterns. Test Stubs are used to verify indirect inputs; Test Spies and Mock Objects are used to verify indirect outputs. Fake objects provide an alternative implementation.

图像

这些变体是根据我们使用测试替身的方式/原因进行分类的。我们将在“实现”部分讨论如何构建测试替身的变体。

These variations are classified based on how/why we use the Test Double. We will deal with variations around how we build the Test Doubles in the "Implementation" section.

变体:测试桩

我们使用测试桩(第 529页) 来替换 SUT 所依赖的实际组件,以便测试拥有 SUT 间接输入的控制点。它的包含允许测试强制 SUT 执行它原本可能不会执行的路径。我们可以根据测试桩用于注入 SUT 的间接输入类型进一步对测试桩进行分类。响应者(参见测试桩) 注入有效值,而破坏者(参见测试桩) 注入错误或异常。

We use a Test Stub (page 529) to replace a real component on which the SUT depends so that the test has a control point for the indirect inputs of the SUT. Its inclusion allows the test to force the SUT down paths it might not otherwise execute. We can further classify Test Stubs by the kind of indirect inputs they are used to inject into the SUT. A Responder (see Test Stub) injects valid values, while a Saboteur (see Test Stub) injects errors or exceptions.

有些人使用术语“测试桩”来表示仅在实际对象或过程可用之前使用的临时实现。为了避免混淆,我更喜欢将这种用法称为临时测试桩(请参阅测试桩)。

Some people use the term "test stub" to mean a temporary implementation that is used only until the real object or procedure becomes available. I prefer to call this usage a Temporary Test Stub (see Test Stub) to avoid confusion.

变体:测试间谍

我们可以使用功能更强大的测试桩版本,即测试间谍第 538页),作为 SUT 间接输出的观察点。与测试桩一样,测试间谍可能需要在响应方法调用时向 SUT 提供值。但是,测试间谍还会在执行 SUT 时捕获其间接输出并保存它们以供测试稍后验证。因此,在许多方面,测试间谍“只是一个”具有一些记录功能的测试桩。虽然测试间谍的基本用途与模拟对象第 544页)相同,但我们使用测试间谍编写的测试风格看起来更像是用测试桩编写的测试。

We can use a more capable version of a Test Stub, the Test Spy (page 538), as an observation point for the indirect outputs of the SUT. Like a Test Stub, a Test Spy may need to provide values to the SUT in response to method calls. The Test Spy, however, also captures the indirect outputs of the SUT as it is exercised and saves them for later verification by the test. Thus, in many ways, the Test Spy is "just a" Test Stub with some recording capability. While a Test Spy is used for the same fundamental purpose as a Mock Object (page 544), the style of test we write using a Test Spy looks much more like a test written with a Test Stub.

变体:模拟对象

我们可以使用Mock 对象作为观察点来验证 SUT执行时的间接输出。通常,Mock 对象还包括测试桩的功能,即如果测试尚未失败,它必须将值返回给 SUT,但重点验证间接输出。因此,Mock对象不仅仅是测试桩加上断言:它的使用方式完全不同。

We can use a Mock Object as an observation point to verify the indirect outputs of the SUT as it is exercised. Typically, the Mock Object also includes the functionality of a Test Stub in that it must return values to the SUT if it hasn't already failed the tests but the emphasis1 is on the verification of the indirect outputs. Therefore, a Mock Object is a lot more than just a Test Stub plus assertions: It is used in a fundamentally different way.

变体:假物体

我们使用伪对象第 551页)来替代测试中真实 DOC 的功能,其原因并非验证 SUT 的间接输入和输出。通常,伪对象实现与真实 DOC 相同的功能,但方式要简单得多。虽然伪对象通常是专门为测试而构建的,但测试不会将其用作控制点或观察点。

We use a Fake Object (page 551) to replace the functionality of a real DOC in a test for reasons other than verification of indirect inputs and outputs of the SUT. Typically, a Fake Object implements the same functionality as the real DOC but in a much simpler way. While a Fake Object is typically built specifically for testing, the test does not use it as either a control point or an observation point.

使用伪对象的最常见原因是真正的 DOC 尚不可用、速度太慢或由于有害的副作用而无法在测试环境中使用。侧栏“无需共享装置即可进行更快的测试”(第 319页)描述了我们如何将所有数据库访问封装在持久层接口后面,然后用内存中的哈希表替换数据库,并使我们的测试运行速度提高 50 倍。第 6 章测试自动化策略”第 11 章使用测试替身”概述了可用于使我们的 SUT 更易于测试的各种技术。

The most common reason for using a Fake Object is that the real DOC is not available yet, is too slow, or cannot be used in the test environment because of deleterious side effects. The sidebar "Faster Tests Without Shared Fixtures" (page 319) describes how we encapsulated all database access behind a persistence layer interface and then replaced the database with in-memory hash tables and made our tests run 50 times faster. Chapter 6, Test Automation Strategy, and Chapter 11, Using Test Doubles, provide an overview of the various techniques available for making our SUT easier to test.

变体:虚拟对象

SUT 的某些方法签名可能需要对象作为参数。如果测试和 SUT 都不关心这些对象,我们可以选择传入一个虚拟对象第 728页),它可以是简单的空对象引用、类的实例或伪对象Object的实例(请参阅第 568页的硬编码测试替身)。从这个意义上讲,虚拟对象实际上并不是测试替身,而是值模式文字值第 714页)、派生值第 718页)和生成值(第723页)的替代品。

Some method signatures of the SUT may require objects as parameters. If neither the test nor the SUT cares about these objects, we may choose to pass in a Dummy Object (page 728), which may be as simple as a null object reference, an instance of the Object class, or an instance of a Pseudo-Object (see Hard-Coded Test Double on page 568). In this sense, a Dummy Object isn't really a Test Double per se but rather an alternative to the value patterns Literal Value (page 714), Derived Value (page 718), and Generated Value (page 723).

变体:程序测试桩

用过程编程语言实现的测试替身通常称为“测试桩”,但我更喜欢称其为过程测试桩(请参阅测试桩),以区别于现代测试替身的测试变体。通常,我们使用过程测试桩来允许在等待其他代码可用时进行测试/调试。这些对象很少在运行时被“交换”,但有时我们会根据“调试”标志(生产中的测试逻辑的一种形式)使代码具有条件性(第 217页)。

A Test Double implemented in a procedural programming language is often called a "test stub," but I prefer to call it a Procedural Test Stub (see Test Stub) to distinguish this usage from the modern Test Stub variation of Test Doubles. Typically, we use a Procedural Test Stub to allow testing/debugging to proceed while waiting for other code to become available. It is rare for these objects to be "swapped in" at runtime but sometimes we make the code conditional on a "Debugging" flag—a form of Test Logic in Production (page 217).

实施说明

Implementation Notes

在构建测试替身时,必须考虑几个因素(图 23.2):

Several considerations must be taken into account when we are building the Test Double (Figure 23.2):

  • 测试替身是否应该专用于单个测试,还是可在多个测试中重复使用
  • Whether the Test Double should be specific to a single test or reusable across many tests
  • 测试替身是否应该存在于代码中,还是动态生成
  • Whether the Test Double should exist in code or be generated on-the-fly
  • 如何告诉 SUT 使用测试替身(安装)
  • How we tell the SUT to use the Test Double (installation)

图 23.2. 测试替身类型及其实现选择。只有测试桩、测试间谍和模拟对象需要由测试进行硬编码或配置。虚拟对象没有实现;伪对象已安装但不受测试控制。

Figure 23.2. Types of Test Doubles with implementation choices. Only Test Stubs, Test Spies, and Mock Objects need to be hard-coded or configured by the test. Dummy Objects have no implementation; Fake Objects are installed but not controlled by the test.

图像

这里讨论了第一点和最后一点。关于测试替身生成的讨论留到可配置测试替身部分

The first and last points are addressed here. The discussion of Test Double generation is left to the section on Configurable Test Doubles.

因为构建测试替身的技术很大程度上独立于它们的行为(例如,它们既适用于测试桩,也适用于模拟对象),我选择将构建硬编码测试替身可配置测试替身(第558页)的各种方式的描述分成单独的模式。

Because the techniques for building Test Doubles are pretty much independent of their behavior (e.g., they apply to both Test Stubs and Mock Objects), I've chosen to split out the descriptions of the various ways we can build Hard-Coded Test Doubles and Configurable Test Doubles (page 558) into separate patterns.

变体:不可配置的测试替身

虚拟对象伪对象都无需配置,各有各的原因。接收方永远不应使用虚拟对象,因此它们不需要“真实”的实现。相比之下,伪对象需要“真实”的实现,但这种实现要比它们所替换的对象简单得多或“轻量”。因此,测试和测试自动化程序都不需要配置“预设”的响应或期望;我们只需安装测试替身,让 SUT 像使用真实对象一样使用它。

Neither Dummy Objects nor Fake Objects need to be configured, each for their own reason. Dummies should never be used by the receiver so they need no "real" implementation. Fake Objects, by contrast, need a "real" implementation but one that is much simpler or "lighter" than the object that they replace. Therefore, neither the test nor the test automater will need to configure "canned" responses or expectations; we just install the Test Double and let the SUT use it as if it were real.

变体:硬编码测试替身

当我们计划在一个测试中只使用一个特定的测试替身时,最简单的方法往往是直接对测试替身进行硬编码,使其返回特定的值(对于测试桩)或期望特定的方法调用(模拟对象)。硬编码测试替身通常由测试自动化程序手工构建。它们有几种形式,包括自分流(参见硬编码测试替身其中测试用例类第 373页)充当测试替身;匿名内部测试替身(参见硬编码测试替身其中使用语言特性在测试方法第 348页)内部创建测试替身;以及作为单独的测试替身类实现的测试替身(参见硬编码测试替身)。硬编码测试替身中对上述每个选项都有更详细的讨论

When we plan to use a specific Test Double in only a single test, it is often simplest to just hard-code the Test Double to return specific values (for Test Stubs) or expect specific method calls (Mock Objects). Hard-Coded Test Doubles are typically hand-built by the test automater. They come in several forms, including the Self Shunt (see Hard-Coded Test Double), where the Testcase Class (page 373) acts as the Test Double; the Anonymous Inner Test Double (see Hard-Coded Test Double), where language features are used to create the Test Double inside the Test Method (page 348); and the Test Double implemented as separate Test Double Class (see Hard-Coded Test Double). Each of these options is discussed in more detail in Hard-Coded Test Double.

变体:可配置测试替身

当我们想在许多测试中使用相同的测试替身实现时,我们通常倾向于使用可配置测试替身。虽然测试自动化程序可以手动构建这些对象,但 xUnit 家族的许多成员都有可重用的工具包可用于生成可配置测试替身

When we want to use the same Test Double implementation in many tests, we will typically prefer to use a Configurable Test Double. Although the test automater can manually build these objects, many members of the xUnit family have reusable toolkits available for generating Configurable Test Doubles.

安装测试替身

在测试 SUT 之前,我们必须告诉它使用测试替身,而不是测试替身所替换的对象。我们可以使用任何可替代的依赖模式,在四阶段测试的夹具设置阶段安装测试替身。在测试 SUT 之前,需要配置可配置测试替身,我们通常在安装它们之前执行此配置。

Before we can exercise the SUT, we must tell it to use the Test Double instead of the object that the Test Double replaces. We can use any of the substitutable dependency patterns to install the Test Double during the fixture setup phase of our Four-Phase Test. Configurable Test Doubles need to be configured before we exercise the SUT, and we typically perform this configuration before we install them.

示例:测试替身

Example: Test Double

由于使用测试替身变体的原因多种多样,因此很难提供一个示例来描述每种风格背后的动机。请参阅前面提到的每个更详细的模式中的示例。

Because there are a wide variety of reasons for using the variations of Test Doubles, it is difficult to provide a single example that characterizes the motivation behind each style. Please refer to the examples in each of the more detailed patterns referenced earlier.

测试桩

Test Stub

也称为

Also known as

Stub

当逻辑依赖于来自其他软件组件的间接输入时,我们如何独立验证逻辑?

How can we verify logic independently when it depends on indirect inputs from other software components?

我们用测试特定的对象替换真实对象,该对象将所需的间接输入提供给被测系统。

We replace a real object with a test-specific object that feeds the desired indirect inputs into the system under test.

图像

在许多情况下,SUT 运行的环境或上下文会极大地影响 SUT 的行为。为了充分控制 SUT 的间接输入,我们可能必须用我们可以控制的东西(即测试桩)替换部分上下文

In many circumstances, the environment or context in which the SUT operates very much influences the behavior of the SUT. To get adequate control over the indirect inputs of the SUT, we may have to replace some of the context with something we can control—namely, a Test Stub.

工作原理

How It Works

首先,我们定义 SUT 所依赖的接口的测试特定实现。此实现配置为使用将在 SUT 内执行未测试代码(请参阅第268页的生产错误)的值(或异常)来响应来自 SUT 的调用。在执行 SUT 之前,我们安装测试桩,以便 SUT 使用它而不是实际实现。当在测试执行期间被 SUT 调用时,测试桩将返回先前定义的值。然后,测试可以以正常方式验证预期结果。

First, we define a test-specific implementation of an interface on which the SUT depends. This implementation is configured to respond to calls from the SUT with the values (or exceptions) that will exercise the Untested Code (see Production Bugs on page 268) within the SUT. Before exercising the SUT, we install the Test Stub so that the SUT uses it instead of the real implementation. When called by the SUT during test execution, the Test Stub returns the previously defined values. The test can then verify the expected outcome in the normal way.

何时使用它

When to Use It

使用测试桩的一个关键迹象是,由于我们无法控制 SUT 的间接输入,导致代码未经测试。我们可以使用测试桩作为控制点,这样我们就可以通过各种间接输入来控制 SUT 的行为,而无需验证间接输出。我们还可以使用测试桩注入值,这样我们就可以越过软件中的某个特定点,在该点,SUT 调用我们测试环境中不可用的软件。

A key indication for using a Test Stub is having Untested Code caused by our inability to control the indirect inputs of the SUT. We can use a Test Stub as a control point that allows us to control the behavior of the SUT with various indirect inputs and we have no need to verify the indirect outputs. We can also use a Test Stub to inject values that allow us to get past a particular point in the software where the SUT calls software that is unavailable in our test environment.

如果我们确实需要一个观察点来验证 SUT 的间接输出,我们应该考虑使用Mock Object第 544页)或Test Spy第 538页)。当然,我们必须有办法将Test Double第 522页)安装到 SUT 中,以便能够使用任何形式的Test Double

If we do need an observation point that allows us to verify the indirect outputs of the SUT, we should consider using a Mock Object (page 544) or a Test Spy (page 538). Of course, we must have a way of installing a Test Double (page 522) into the SUT to be able to use any form of Test Double.

变体:应答器

用于将有效的间接输入注入 SUT 以便其可以执行其业务的测试桩称为响应器。当实际组件无法控制、尚不可用或在开发环境中不可用时,响应器通常用于“快乐路径”测试。测试将始终是简单成功测试(请参阅第348页的测试方法)。

A Test Stub that is used to inject valid indirect inputs into the SUT so that it can go about its business is called a Responder. Responders are commonly used in "happy path" testing when the real component is uncontrollable, is not yet available, or is unusable in the development environment. The tests will invariably be Simple Success Tests (see Test Method on page 348).

变体:破坏者

用于向 SUT 注入无效间接输入的测试桩通常称为破坏者因为它的目的是破坏 SUT 试图执行的任何操作,以便我们了解 SUT 在这些情况下如何应对。“破坏”可能是由返回意外值或对象引起的,也可能是由引发异常或导致运行时错误引起的。每个测试可能是简单成功测试预期异常测试(参见测试方法具体取决于 SUT 响应间接输入的预期行为。

A Test Stub that is used to inject invalid indirect inputs into the SUT is often called a Saboteur because its purpose is to derail whatever the SUT is trying to do so that we can see how the SUT copes under these circumstances. The "derailment" might be caused by returning unexpected values or objects, or it might result from raising an exception or causing a runtime error. Each test may be either a Simple Success Test or an Expected Exception Test (see Test Method), depending on how the SUT is expected to behave in response to the indirect input.

变体:临时测试桩

临时测试桩代替尚未可用的 DOC。这种测试桩通常由真实类的空壳和硬编码的返回语句组成。一旦真实 DOC 可用,它就会替换临时测试桩。测试驱动开发通常要求我们在从外向内编写代码时创建临时测试桩;随着我们向这些壳中添加代码,它们会演变为真实类。在需求驱动开发中,我们倾向于使用模拟对象,因为我们想要验证 SUT 是否在临时测试桩上调用了正确的方法;此外,即使在真实 DOC 可用后,我们通常仍会继续使用模拟对象

A Temporary Test Stub stands in for a DOC that is not yet available. This kind of Test Stub typically consists of an empty shell of a real class with hard-coded return statements. As soon as the real DOC is available, it replaces the Temporary Test Stub. Test-driven development often requires us to create Temporary Test Stubs as we write code from the outside in; these shells evolve into the real classes as we add code to them. In need-driven development, we tend to use Mock Objects because we want to verify that the SUT calls the right methods on the Temporary Test Stub; in addition, we typically continue using the Mock Object even after the real DOC becomes available.

变体:程序测试桩

过程测试桩是用过程编程语言编写的测试桩。在不支持过程变量(也称为函数指针)的过程编程语言中创建过程测试桩尤其困难。在大多数情况下,我们必须将钩子放入生产代码中(生产中的测试逻辑if  testing  then的一种形式;参见第217页)。

A Procedural Test Stub is a Test Stub written in a procedural programming language. It is particularly challenging to create in procedural programming languages that do not support procedure variables (also known as function pointers). In most cases, we must put if  testing  then hooks into the production code (a form of Test Logic in Production; see page 217).

变体:实体链剪切

实体链剪切(请参阅第 529页的测试桩)是响应器的一个特殊情况,用于用一个假装是对象网络的测试桩替换复杂的对象网络。它的加入可以使夹具设置过程更快(特别是当对象通常必须持久保存到数据库中时),并且可以使测试更容易理解。

Entity Chain Snipping (see Test Stub on page 529) is a special case of a Responder that is used to replace a complex network of objects with a single Test Stub that pretends to be the network of objects. Its inclusion can make fixture setup go much more quickly (especially when the objects would normally have to be persisted into a database) and can make the tests much easier to understand.

实施说明

Implementation Notes

使用测试桩时必须小心,因为我们测试 SUT 的配置与生产中使用的配置不同。我们确实应该至少有一个测试来验证 SUT 在没有测试桩的情况下是否可以工作。刚接触桩的测试自动化人员常犯的一个错误是替换他们试图测试的 SUT 的一部分。因此,真正清楚什么在扮演 SUT 的角色,什么在扮演测试装置的角色是很重要的。另请注意,过度使用测试可能会导致过度指定软件(请参阅第239页的脆弱测试)。

We must be careful when using Test Stubs because we are testing the SUT in a different configuration from the one that will be used in production. We really should have at least one test that verifies the SUT works without a Test Stub. A common mistake made by test automaters who are new to stubs is to replace a part of the SUT that they are trying to test. For this reason, it is important to be really clear about what is playing the role of SUT and what is playing the role of test fixture. Also, note that excessive use of Test Stubs can result in Overspecified Software (see Fragile Test on page 239).

根据我们的具体需求和现有的工具,测试桩可以用几种不同的方式构建。

Test Stubs may be built in several different ways depending on our specific needs and the tools we have on hand.

变体:硬编码测试桩

硬编码测试桩的响应是在其程序逻辑中硬编码的。这些测试桩往往是为单个测试或极少量测试而专门构建的。有关更多信息,请参阅硬编码测试替身(第568页)。

A Hard-Coded Test Stub has its responses hard-coded within its program logic. These Test Stubs tend to be purpose-built for a single test or a very small number of tests. See Hard-Coded Test Double (page 568) for more information.

变体:可配置测试桩

当我们想避免为每个测试构建不同的硬编码测试桩时,我们可以使用可配置测试桩(请参阅第558页的可配置测试替身)。测试将可配置测试桩配置为其夹具设置阶段的一部分。xUnit 系列的许多成员都提供了用于生成可配置测试替身第 558页)的工具,包括可配置测试桩

When we want to avoid building a different Hard-Coded Test Stub for each test, we can use a Configurable Test Stub (see Configurable Test Double on page 558). A test configures the Configurable Test Stub as part of its fixture setup phase. Many members of the xUnit family offer tools with which to generate Configurable Test Doubles (page 558), including Configurable Test Stubs.

激励人心的例子

Motivating Example

以下测试验证了格式化包含当前时间的 HTML 字符串的组件的基本功能。不幸的是,它依赖于实际系统时钟,因此很少能通过!

The following test verifies the basic functionality of a component that formats an HTML string containing the current time. Unfortunately, it depends on the real system clock so it rarely ever passes!

public void testDisplayCurrentTime_AtMidnight() {

    // 固定设置

    TimeDisplay sut = new TimeDisplay();

    // 练习 SUT

    String result = sut.getCurrentTimeAsHtmlFragment();

    // 验证直接输出

    String expectedTimeString =

             "<span class=\"tinyBoldText\">Midnight</span>";

    assertEquals( expectedTimeString, result);

}

public  void  testDisplayCurrentTime_AtMidnight()  {

    //  fixture  setup

    TimeDisplay  sut  =  new  TimeDisplay();

    //  exercise  SUT

    String  result  =  sut.getCurrentTimeAsHtmlFragment();

    //  verify  direct  output

    String  expectedTimeString  =

             "<span  class=\"tinyBoldText\">Midnight</span>";

    assertEquals(  expectedTimeString,  result);

}

 

我们可以尝试通过让测试根据当前系统时间计算预期结果来解决这个问题,如下所示:

We could try to address this problem by making the test calculate the expected results based on the current system time as follows:

public void testDisplayCurrentTime_whenever() {

     // 固定设置

     TimeDisplay sut = new TimeDisplay();

     // 练习 SUT

     String result = sut.getCurrentTimeAsHtmlFragment();

     // 验证结果

     Calendar time = new DefaultTimeProvider().getTime();

     StringBuffer expectedTime = new StringBuffer();

     expectedTime.append("<span class=\"tinyBoldText\">");

     if ((time.get(Calendar.HOUR_OF_DAY) == 0)

           && (time.get(Calendar.MINUTE) <= 1)) {

        expectedTime.append( "午夜");

     } else if ((time.get(Calendar.HOUR_OF_DAY) == 12)

                       && (time.get(Calendar.MINUTE) == 0)) { // 中午

          expectedTime.append("N3oon");

      } else {

            SimpleDateFormat fr = new SimpleDateFormat("h:mm a");

            expectedTime.append(fr.format(time.getTime()));

      }

      expectedTime.append("</span>");



      assertEquals( expectedTime, result);

}

public  void  testDisplayCurrentTime_whenever()  {

     //  fixture  setup

     TimeDisplay  sut  =  new  TimeDisplay();

     //  exercise  SUT

     String  result  =  sut.getCurrentTimeAsHtmlFragment();

     //  verify  outcome

     Calendar  time  =  new  DefaultTimeProvider().getTime();

     StringBuffer  expectedTime  =  new  StringBuffer();

     expectedTime.append("<span  class=\"tinyBoldText\">");

     if  ((time.get(Calendar.HOUR_OF_DAY)  ==  0)

           &&  (time.get(Calendar.MINUTE)  <=  1))  {

        expectedTime.append(  "Midnight");

     }  else  if  ((time.get(Calendar.HOUR_OF_DAY)  ==  12)

                       &&  (time.get(Calendar.MINUTE)  ==  0))  {  //  noon

          expectedTime.append("N3oon");

      }  else    {

            SimpleDateFormat  fr  =  new  SimpleDateFormat("h:mm  a");

            expectedTime.append(fr.format(time.getTime()));

      }

      expectedTime.append("</span>");



      assertEquals(  expectedTime,  result);

}

 

这种灵活的测试(请参阅第 200页的条件测试逻辑)引入了两个问题。首先,一些测试条件从未执行过。(想在午夜来上班运行测试以证明软件在午夜工作吗?)其次,测试需要复制 SUT 中的大部分逻辑来计算预期结果。我们如何证明逻辑确实正确?

This Flexible Test (see Conditional Test Logic on page 200) introduces two problems. First, some test conditions are never exercised. (Do you want to come in to work to run the tests at midnight to prove the software works at midnight?) Second, the test needs to duplicate much of the logic in the SUT to calculate the expected results. How do we prove the logic is actually correct?

重构说明

Refactoring Notes

我们可以通过控制时间来实现对间接输入的正确验证。为此,我们使用“用测试替身替换依赖项” (第522页) 重构,将真实系统时钟(此处表示为TimeProvider)替换为虚拟时钟[VCTP]。然后,我们将其实现为测试桩,该桩由测试配置,并带有我们想要用作 SUT 间接输入的时间。

We can achieve proper verification of the indirect inputs by getting control of the time. To do so, we use the Replace Dependency with Test Double (page 522) refactoring to replace the real system clock (represented here by TimeProvider) with a Virtual Clock [VCTP]. We then implement it as a Test Stub that is configured by the test with the time we want to use as the indirect input to the SUT.

示例:响应程序(作为手工编码的测试桩)

Example: Responder (as Hand-Coded Test Stub)

以下测试使用Responder来控制 SUT 的间接输入,以验证其中一个令人满意的路径测试条件。根据注入 SUT 的时间,可以安全地对预期结果进行硬编码。

The following test verifies one of the happy path test conditions using a Responder to get control over the indirect inputs of the SUT. Based on the time injected into the SUT, the expected result can be hard-coded safely.

public void testDisplayCurrentTime_AtMidnight()

                   throws Exception {

    // Fixture 设置

    // 测试替身配置

    TimeProviderTestStub tpStub = new TimeProviderTestStub();

    tpStub.setHours(0);

    tpStub.setMinutes(0);

    // 实例化 SUT

    TimeDisplay sut = new TimeDisplay();

    // 测试替身安装

    sut.setTimeProvider(tpStub);

    // 练习 SUT

    String result = sut.getCurrentTimeAsHtmlFragment();

    // 验证结果

    String expectedTimeString =

                 "<span class=\"tinyBoldText\">Midnight</span>";

    assertEquals("Midnight", expectedTimeString, result);

}

public  void  testDisplayCurrentTime_AtMidnight()

                   throws  Exception  {

    //  Fixture  setup

    //         Test  Double  configuration

    TimeProviderTestStub  tpStub  =  new  TimeProviderTestStub();

    tpStub.setHours(0);

    tpStub.setMinutes(0);

    //      Instantiate  SUT

    TimeDisplay  sut  =  new  TimeDisplay();

    //            Test  Double  installation

    sut.setTimeProvider(tpStub);

    //  Exercise  SUT

    String  result  =  sut.getCurrentTimeAsHtmlFragment();

    //  Verify  outcome

    String  expectedTimeString  =

                 "<span  class=\"tinyBoldText\">Midnight</span>";

    assertEquals("Midnight",  expectedTimeString,  result);

}

 

该测试使用以下手工编码的可配置测试桩实现:

This test makes use of the following hand-coded configurable Test Stub implementation:

private Calendar myTime = new GregorianCalendar();

/**

* TimeProviderTestStub 的完整构造函数

* @param hours 使用 24 小时制指定小时数

* (例如,10 = 上午 10 点、12 = 中午、22 = 晚上 10 点、0 = 午夜)

* @param minutes 指定小时后的分钟数

* (例如,0 = 整点,1 = 整点后 1 分钟)

*/

public TimeProviderTestStub(int hours, int minutes) {

    setTime(hours, minutes);

}

public void setTime(int hours, int minutes) {

    setHours(hours);

    setMinutes(minutes);

}

// 配置接口

public void setHours(int hours) {

      // 0 表示午夜;12 表示中午

      myTime.set(Calendar.HOUR_OF_DAY, hours);

}

public void setMinutes(int minutes) {

      myTime.set(Calendar.MINUTE, minutes);

}

// SUT 使用的接口

public Calendar getTime() {

    // @return 最后设置的时间

    return myTime;

}

private  Calendar  myTime  =  new  GregorianCalendar();

/**

*  The  complete  constructor  for  the  TimeProviderTestStub

*  @param  hours  specifies  the  hours  using  a  24-hour  clock

*        (e.g.,  10  =  10  AM,  12  =  noon,  22  =  10  PM,  0  =  midnight)

*  @param  minutes  specifies  the  minutes  after  the  hour

*      (e.g.,  0  =  exactly  on  the  hour,  1  =  1  min  after  the  hour)

*/

public  TimeProviderTestStub(int  hours,  int  minutes)  {

    setTime(hours,  minutes);

}

public  void  setTime(int  hours,  int  minutes)  {

    setHours(hours);

    setMinutes(minutes);

}

//  Configuration  interface

public  void  setHours(int  hours)  {

      //  0  is  midnight;  12  is  noon

      myTime.set(Calendar.HOUR_OF_DAY,  hours);

}

public  void  setMinutes(int  minutes)  {

      myTime.set(Calendar.MINUTE,  minutes);

}

//  Interface  used  by  SUT

public  Calendar  getTime()  {

    //  @return  the  last  time  that  was  set

    return  myTime;

}

 

示例:响应程序(动态生成)

Example: Responder (Dynamically Generated)

以下是使用 JMock可配置测试替身框架编写的相同测试:

Here's the same test coded using the JMock Configurable Test Double framework:

public void testDisplayCurrentTime_AtMidnight_JM()

         throws Exception {

    // Fixture 设置

    TimeDisplay sut = new TimeDisplay();

    // 测试替身配置

    Mock tpStub = mock(TimeProvider.class);

    Calendar midnight = makeTime(0,0);

    tpStub.stubs().method("getTime").

                          withNoArguments().

                          will(returnValue(midnight));

    // 测试替身安装

    sut.setTimeProvider((TimeProvider) tpStub);

    // 练习 SUT

    String result = sut.getCurrentTimeAsHtmlFragment();

    // 验证结果

    String expectedTimeString =

                "<span class=\"tinyBoldText\">Midnight</span>";

    assertEquals("Midnight", expectedTimeString, result);

}

public  void  testDisplayCurrentTime_AtMidnight_JM()

         throws  Exception  {

    //  Fixture  setup

    TimeDisplay  sut  =  new  TimeDisplay();

    //    Test  Double  configuration

    Mock  tpStub  =  mock(TimeProvider.class);

    Calendar  midnight  =  makeTime(0,0);

    tpStub.stubs().method("getTime").

                          withNoArguments().

                          will(returnValue(midnight));

    //    Test  Double  installation

    sut.setTimeProvider((TimeProvider)  tpStub);

    //  Exercise  SUT

    String  result  =  sut.getCurrentTimeAsHtmlFragment();

    //  Verify  outcome

    String  expectedTimeString  =

                "<span  class=\"tinyBoldText\">Midnight</span>";

    assertEquals("Midnight",  expectedTimeString,  result);

}

 

此测试没有要检查的测试桩实现,因为 JMock 框架使用反射来实现测试桩。因此,我们必须编写一个名为的测试实用方法第 599页)makeTime,其中包含构造要返回的对象的逻辑Calendar。在手工编码的测试桩中,此逻辑出现在getTime方法内部。

There is no Test Stub implementation to examine for this test because the JMock framework implements the Test Stub using reflection. Thus we had to write a Test Utility Method (page 599) called makeTime that contains the logic to construct the Calendar object to be returned. In the hand-coded Test Stub, this logic appeared inside the getTime method.

示例:Saboteur(作为匿名内部类)

Example: Saboteur (as Anonymous Inner Class)

以下测试使用Saboteur向 SUT 注入无效的间接输入,以便我们了解 SUT 在这些情况下如何应对。

The following test uses a Saboteur to inject invalid indirect inputs into the SUT so we can see how the SUT copes under these circumstances.

public void testDisplayCurrentTime_exception()

            throws Exception {

      // Fixture 设置

      // 定义并实例化测试桩

      TimeProvider testStub = new TimeProvider()

            { // 匿名内部测试桩

                  public Calendar getTime() throws TimeProviderEx {

                        throw new TimeProviderEx("Sample");

            }

      };

      // 实例化 SUT

      TimeDisplay sut = new TimeDisplay();

      sut.setTimeProvider(testStub);

      // 练习 SUT

      String result = sut.getCurrentTimeAsHtmlFragment();

      // 验证直接输出

      String expectedTimeString =

                  "<span class=\"error\">Invalid Time</span>";

      assertEquals("Exception", expectedTimeString, result);

}

public  void  testDisplayCurrentTime_exception()

            throws  Exception  {

      //  Fixture  setup

      //      Define  and  instantiate  Test  Stub

      TimeProvider  testStub  =  new  TimeProvider()

            {  //  Anonymous  inner  Test  Stub

                  public  Calendar  getTime()  throws  TimeProviderEx  {

                        throw  new  TimeProviderEx("Sample");

            }

      };

      //      Instantiate  SUT

      TimeDisplay  sut  =  new  TimeDisplay();

      sut.setTimeProvider(testStub);

      //  Exercise  SUT

      String  result  =  sut.getCurrentTimeAsHtmlFragment();

      //  Verify  direct  output

      String  expectedTimeString  =

                  "<span  class=\"error\">Invalid  Time</span>";

      assertEquals("Exception",  expectedTimeString,  result);

}

 

在本例中,我们使用了内部测试替身(参见硬编码测试替身)来抛出我们期望 SUT 能够妥善处理的异常。此测试的一个有趣之处在于,它使用简单成功测试方法模板而不是预期异常测试模板,尽管我们注入了异常作为间接输入。这种选择背后的理由是,我们期望 SUT 捕获异常并更改字符串格式;我们并不期望 SUT 抛出异常。

In this case, we used an Inner Test Double (see Hard-Coded Test Double) to throw an exception that we expect the SUT to handle gracefully. One interesting thing about this test is that it uses the Simple Success Test method template rather than the Expected Exception Test template, even though we are injecting an exception as the indirect input. The rationale behind this choice is that we are expecting the SUT to catch the exception and change the string formatting; we are not expecting the SUT to throw an exception.

示例:实体链剪切

Example: Entity Chain Snipping

在这个例子中,我们正在测试,Invoice但需要一个Customer来实例化InvoiceCustomer需要一个Address,而这又需要一个City。因此,我们发现自己创建了许多额外的对象只是为了设置夹具。假设发票的行为取决于的某些属性,这些属性是通过调用的方法Customer来计算的。Addressget_zoneCustomer

In this example, we are testing the Invoice but require a Customer to instantiate the Invoice. The Customer requires an Address, which in turn requires a City. Thus we find ourselves creating numerous additional objects just to set up the fixture. Suppose the behavior of the invoice depends on some attribute of the Customer that is calculated from the Address by calling the method get_zone on the Customer.

public void testInvoice_addLineItem_noECS() {

      final int QUANTITY = 1;

      产品 product = new Product(getUniqueNumberAsString(),

                                                          getUniqueNumber());

      州 state = new State("West Dakota", "WD");

      城市 city = new City("Centreville", state);

      地址 address = new Address("123 Blake St.", city, "12345");

      客户 customer= new Customer(getUniqueNumberAsString(),

                                                                getUniqueNumberAsString(),

                                                                address);

      发票 inv = new Invoice(customer);

      // 练习

      inv.addItemQuantity(product, QUANTITY);

      // 验证

      列表 lineItems = inv.getLineItems();

      assertEquals("number of items", lineItems.size(), 1);

      LineItem actual = (LineItem)lineItems.get(0);

      LineItem expItem = new LineItem(inv, product, QUANTITY);

      断言LineItemsEqual(“”,expItem,实际);

}

public  void  testInvoice_addLineItem_noECS()  {

      final  int  QUANTITY  =  1;

      Product  product  =  new  Product(getUniqueNumberAsString(),

                                                          getUniqueNumber());

      State  state  =  new  State("West  Dakota",  "WD");

      City  city  =  new  City("Centreville",  state);

      Address  address  =  new  Address("123  Blake  St.",  city,  "12345");

      Customer  customer=  new  Customer(getUniqueNumberAsString(),

                                                                getUniqueNumberAsString(),

                                                                address);

      Invoice  inv  =  new  Invoice(customer);

      //  Exercise

      inv.addItemQuantity(product,  QUANTITY);

      //  Verify

      List  lineItems  =  inv.getLineItems();

      assertEquals("number  of  items",  lineItems.size(),  1);

      LineItem  actual  =  (LineItem)lineItems.get(0);

      LineItem  expItem  =  new  LineItem(inv,  product,  QUANTITY);

      assertLineItemsEqual("",expItem,  actual);

}

 

在这个测试中,我们只想验证依赖于此zone属性的发票逻辑的行为,而不是根据的地址计算此属性的方式Customer。(有单独的Customer单元测试来验证zone是否正确计算。)所有地址、城市和其他信息的设置只会分散读者的注意力。

In this test, we want to verify only the behavior of the invoice logic that depends on this zone attribute—not the way this attribute is calculated from the Customer's address. (There are separate Customer unit tests to verify the zone is calculated correctly.) All of the setup of the address, city, and other information merely distracts the reader.

这是使用测试桩而不是进行的相同测试Customer。请注意,由于实体链剪切,夹具设置变得多么简单!

Here's the same test using a Test Stub instead of the Customer. Note how much simpler the fixture setup has become as a result of Entity Chain Snipping!

public void testInvoice_addLineItem_ECS() {

      final int QUANTITY = 1;

      Product product = new Product(getUniqueNumberAsString(),

                                                           getUniqueNumber());

      Mock customerStub = mock(ICustomer.class);

      customerStub.stubs().method("getZone").will(returnValue(ZONE_3));

      Invoice inv = new Invoice((ICustomer)customerStub.proxy());

      // 练习

      inv.addItemQuantity(product, QUANTITY);

      // 验证

      列表 lineItems = inv.getLineItems();

      assertEquals("number of items", lineItems.size(), 1);

      LineItem actual = (LineItem)lineItems.get(0);

      LineItem expItem = new LineItem(inv, product, QUANTITY);

      assertLineItemsEqual("", expItem, actual);

}

public  void  testInvoice_addLineItem_ECS()  {

      final  int  QUANTITY  =  1;

      Product  product  =  new  Product(getUniqueNumberAsString(),

                                                           getUniqueNumber());

      Mock  customerStub  =  mock(ICustomer.class);

      customerStub.stubs().method("getZone").will(returnValue(ZONE_3));

      Invoice  inv  =  new  Invoice((ICustomer)customerStub.proxy());

      //  Exercise

      inv.addItemQuantity(product,  QUANTITY);

      //  Verify

      List  lineItems  =  inv.getLineItems();

      assertEquals("number  of  items",  lineItems.size(),  1);

      LineItem  actual  =  (LineItem)lineItems.get(0);

      LineItem  expItem  =  new  LineItem(inv,  product,  QUANTITY);

      assertLineItemsEqual("",  expItem,  actual);

}

 

我们已经使用 JMock 来桩,Customer并在调用时customerStub返回。这就是我们验证行为所需的全部内容,并且我们已经设法摆脱了所有令人分心的额外对象构造。通过阅读此测试,还可以更清楚地看到,发票行为仅取决于返回的值,而不取决于或的任何其他属性。ZONE_3getZoneInvoiceget_zoneCustomerAddress

We have used JMock to stub out the Customer with a customerStub that returns ZONE_3 when getZone is called. This is all we need to verify the Invoice behavior, and we have managed to get rid of all that distracting extra object construction. It is also much clearer from reading this test that invoicing behavior depends only on the value returned by get_zone and not any other attributes of the Customer or Address.

进一步阅读

几乎每本关于使用 xUnit 进行自动化测试的书都提到了测试桩,所以我不会在这里列出这些资源。但是,当您阅读其他书籍时,请记住术语测试桩通常用于指代模拟对象Mocks、Fakes、Stubs 和 Dummies(在附录 B中)包含对各种书籍和文章中使用的术语的更彻底的比较。

Almost every book on automated testing using xUnit has something to say about Test Stubs, so I won't list those resources here. As you are reading other books, however, keep in mind that the term Test Stub is often used to refer to a Mock Object. Mocks, Fakes, Stubs, and Dummies (in Appendix B) contains a more thorough comparison of the terminology used in various books and articles.

Sven Gorts 描述了使用测试桩 [UTwHCM] 的多种不同方式。我采用了他的许多名称,并修改了一些名称以更好地适应这种模式语言。Paolo Perrotta 编写了一个模式,描述了一种称为虚拟时钟的常见响应器示例。他使用测试桩作为真实系统时钟的装饰器[GOF],允许“冻结”或恢复时间。当然,对于大多数测试,我们同样可以轻松地使用硬编码测试桩可配置测试桩。

Sven Gorts describes a number of different ways we can use a Test Stub [UTwHCM]. I have adopted many of his names and adapted a few to better fit into this pattern language. Paolo Perrotta wrote a pattern describing a common example of a Responder called Virtual Clock. He uses a Test Stub as a Decorator [GOF] for the real system clock that allows the time to be "frozen" or resumed. Of course, we could use a Hard-Coded Test Stub or a Configurable Test Stub just as easily for most tests.

测试间谍

Test Spy

也称为

Also known as

间谍,录音测试桩

Spy, Recording Test Stub

我们如何实现行为验证?

当逻辑对其他软件组件有间接输出时,我们如何独立验证逻辑?

How do we implement Behavior Verification?

How can we verify logic independently when it has indirect outputs to other software components?

我们使用测试替身来捕获 SUT 对另一个组件进行的间接输出调用,以便稍后进行测试验证。

We use a Test Double to capture the indirect output calls made to another component by the SUT for later verification by the test.

图像

在许多情况下,SUT 运行的环境或上下文会极大地影响 SUT 的行为。为了充分了解 SUT 的间接输出,我们可能必须用一些可以用来捕获 SUT 输出的内容来替换部分上下文。

In many circumstances, the environment or context in which the SUT operates very much influences the behavior of the SUT. To get adequate visibility of the indirect outputs of the SUT, we may have to replace some of the context with something we can use to capture these outputs of the SUT.

使用测试间谍是一种简单而直观的方法,通过观察点公开 SUT 的间接输出以便对其进行验证,从而实现行为验证第 468页)。

Use of a Test Spy is a simple and intuitive way to implement Behavior Verification (page 468) via an observation point that exposes the indirect outputs of the SUT so they can be verified.

工作原理

How It Works

在执行 SUT 之前,我们会安装一个Test Spy作为 SUT 使用的 DOC 的替代品。Test Spy旨在充当观察点,通过记录 SUT 在执行过程中对其发出的方法调用。在结果验证阶段,测试会将 SUT 传递给Test Spy 的实际值与测试预期值进行比较。

Before we exercise the SUT, we install a Test Spy as a stand-in for a DOC used by the SUT. The Test Spy is designed to act as an observation point by recording the method calls made to it by the SUT as it is exercised. During the result verification phase, the test compares the actual values passed to the Test Spy by the SUT with the values expected by the test.

何时使用它

When to Use It

使用测试间谍的一个关键迹象是存在未经测试的需求(请参阅第268页上的“生产错误”),这是由于无法观察调用方法对 SUT 的副作用而导致的。测试间谍是一种自然而直观的扩展现有测试以覆盖这些间接输出的方法,因为对断言方法(第 362页) 的调用是在 SUT 执行后由测试调用的,就像在“正常”测试中一样。测试间谍仅充当观察点,使测试方法(第 348页) 能够访问 SUT 执行期间记录的值。

A key indication for using a Test Spy is having an Untested Requirement (see Production Bugs on page 268) caused by an inability to observe the side effects of invoking methods on the SUT. Test Spies are a natural and intuitive way to extend the existing tests to cover these indirect outputs because the calls to the Assertion Methods (page 362) are invoked by the test after the SUT has been exercised just like in "normal" tests. The Test Spy merely acts as the observation point that gives the Test Method (page 348) access to the values recorded during the SUT execution.

我们应该在下列情况下使用测试间谍:

We should use a Test Spy in the following circumstances:

  • 我们正在验证 SUT 的间接输出,并且无法提前预测与 SUT 交互的所有属性的值。
  • We are verifying the indirect outputs of the SUT and we cannot predict the values of all attributes of the interactions with the SUT ahead of time.
  • 我们希望断言在测试中可见,并且我们认为Mock Object第 544页)期望的建立方式不足以揭示意图。
  • We want the assertions to be visible in the test and we don't think the way in which the Mock Object (page 544) expectations are established is sufficiently intent-revealing.
  • 我们的测试需要测试特定的相等性(因此我们不能使用在 SUT 中实现的相等性的标准定义),并且我们正在使用生成Mock 对象的工具,但没有让我们控制被调用的断言方法。
  • Our test requires test-specific equality (so we cannot use the standard definition of equality as implemented in the SUT) and we are using tools that generate the Mock Object but do not give us control over the Assertion Methods being called.
  • 失败的断言无法有效地报告回测试运行器第 377页)。如果 SUT 在捕获所有异常的容器内运行,并且难以报告结果,或者 SUT 的逻辑在与调用它的测试不同的线程或进程中运行,则可能会发生这种情况。(这两种情况实际上都需要重构,以便我们直接测试 SUT 逻辑,但这是另一章的主题。)
  • A failed assertion cannot be reported effectively back to the Test Runner (page 377). This might occur if the SUT is running inside a container that catches all exceptions and makes it difficult to report the results or if the logic of the SUT runs in a different thread or process from the test that invokes it. (Both of these cases really beg refactoring to allow us to test the SUT logic directly, but that is the subject of another chapter.)
  • 我们希望在对 SUT 的所有外拨呼叫做出任何断言之前能够访问它们。
  • We would like to have access to all the outgoing calls of the SUT before making any assertions on them.

如果这些标准都不适用,我们可能需要考虑使用模拟对象。如果我们试图通过控制 SUT 的间接输入来解决未测试代码(参见生产错误),那么一个简单的测试桩(第529页)可能就是我们所需要的。

If none of these criteria apply, we may want to consider using a Mock Object. If we are trying to address Untested Code (see Production Bugs) by controlling the indirect inputs of the SUT, a simple Test Stub (page 529) may be all we need.

与 Mock 对象不同测试间谍不会在第一次偏离预期行为时就导致测试失败。因此,我们的测试将能够根据Mock 对象测试失败后收集的信息,在断言消息(第 370页) 中包含更详细的诊断信息。然而,在测试失败时,只有测试方法本身中的信息可用于调用断言方法。如果我们需要包含仅在 SUT 运行时才可访问的信息,我们必须在测试间谍中明确捕获它,或者必须使用Mock 对象

Unlike a Mock Object, a Test Spy does not fail the test at the first deviation from the expected behavior. Thus our tests will be able to include more detailed diagnostic information in the Assertion Message (page 370) based on information gathered after a Mock Object would have failed the test. At the point of test failure, however, only the information within the Test Method itself is available to be used in the calls to the Assertion Methods. If we need to include information that is accessible only while the SUT is being exercised, either we must explicitly capture it within our Test Spy or we must use a Mock Object.

当然,除非 SUT 实现某种形式的可替代依赖关系,否则我们将无法使用任何测试替身第 522页)。

Of course, we won't be able to use any Test Doubles (page 522) unless the SUT implements some form of substitutable dependency.

实施说明

Implementation Notes

测试间谍本身可以构建为硬编码测试替身第 568页)或可配置测试替身第 558页)。由于这些模式的讨论中出现了详细示例,因此这里仅提供简要概述。同样,在执行 SUT之前,我们可以使用任何可替代的依赖模式来安装测试间谍。

The Test Spy itself can be built as a Hard-Coded Test Double (page 568) or as a Configurable Test Double (page 558). Because detailed examples appear in the discussion of those patterns, only a quick summary is provided here. Likewise, we can use any of the substitutable dependency patterns to install the Test Spy before we exercise the SUT.

测试使用Test Spy 的关键特性与断言是在测试方法中进行的事实有关。因此,测试必须先恢复Test Spy捕获的间接输出,然后才能进行断言,这可以通过多种方式完成。

The key characteristic in how a test uses a Test Spy relates to the fact that assertions are made from within the Test Method. Therefore, the test must recover the indirect outputs captured by the Test Spy before it can make its assertions, which can be done in several ways.

变体:检索界面

我们可以将测试间谍定义为一个单独的类,该类具有一个检索接口,用于公开记录的信息。测试方法在测试的装置设置阶段安装测试间谍而不是正常的 DOC。测试执行完 SUT 后,它使用检索接口从测试间谍中检索 SUT 的实际间接输出,然后使用这些输出作为参数调用断言方法。

We can define the Test Spy as a separate class with a Retrieval Interface that exposes the recorded information. The Test Method installs the Test Spy instead of the normal DOC as part of the fixture setup phase of the test. After the test has exercised the SUT, it uses the Retrieval Interface to retrieve the actual indirect outputs of the SUT from the Test Spy and then calls Assertion Methods with those outputs as arguments.

变体:自分流

也称为

Also known as

回送

Loopback

我们可以将测试间谍测试用例类(第 373页) 合并为一个称为自我分流器 (Self Shunt ) 的对象。测试方法将自身(测试用例对象(第 382页))作为 DOC 安装到 SUT 中。每当 SUT 委托给 DOC 时,它实际上都是在调用测试用例对象上的方法,测试用例对象通过将实际值保存到可由测试方法访问的实例变量中来实现方法。方法还可以在测试间谍方法中做出断言,在这种情况下,自我分流器是模拟对象而不是测试间谍的变体。在静态类型语言中,测试用例类必须实现 SUT 所依赖的传出接口(观察点),以便测试用例类与用于保存 DOC 的变量类型兼容。

We can collapse the Test Spy and the Testcase Class (page 373) into a single object called a Self Shunt. The Test Method installs itself, the Testcase Object (page 382), as the DOC into the SUT. Whenever the SUT delegates to the DOC, it is actually calling methods on the Testcase Object, which implements the methods by saving the actual values into instance variables that can be accessed by the Test Method. The methods could also make assertions in the Test Spy methods, in which case the Self Shunt is a variation on a Mock Object rather than a Test Spy. In statically typed languages, the Testcase Class must implement the outgoing interface (the observation point) on which the SUT depends so that the Testcase Class is type-compatible with the variables that are used to hold the DOC.

变体:内部测试替身

将测试间谍实现为硬编码测试替身的一种流行方法是将其编码为测试方法中的匿名内部类块闭包,并让此类或将实际值保存到测试方法可访问的实例或局部变量中这种变体实际上是实现自分流的另一种方法(请参阅硬编码测试替身)。

A popular way to implement the Test Spy as a Hard-Coded Test Double is to code it as an anonymous inner class or block closure within the Test Method and to have this class or block save the actual values into instance or local variables that are accessible by the Test Method. This variation is really another way to implement a Self Shunt (see Hard-Coded Test Double).

变体:间接输出注册表

还有一种可能性是让测试间谍将实际参数存储在测试方法可以访问的众所周知的位置。例如,测试间谍可以将这些值保存在文件或注册表[PEAA]对象中。

Yet another possibility is to have the Test Spy store the actual parameters in a well-known place where the Test Method can access them. For example, the Test Spy could save those values in a file or in a Registry [PEAA] object.

激励人心的例子

Motivating Example

以下测试验证了删除航班的基本功能,但未验证 SUT 的间接输出 - 即,SUT 预计每次删除航班时都会记录请求者的日期/时间和用户名。

The following test verifies the basic functionality of removing a flight but does not verify the indirect outputs of the SUT—namely, the fact that the SUT is expected to log each time a flight is removed along with the date/time and username of the requester.

public void testRemoveFlight() throws Exception {

    // 设置

    FlightDto expectedFlightDto = createARegisteredFlight();

    FlightManagementFacade Facade = new FlightManagementFacadeImpl();

    // 练习

    Facade.removeFlight(expectedFlightDto.getFlightNumber());

    // 验证

    assertFalse("flight 在被移除后不应存在",

                     Facade.flightExists( expectedFlightDto.

                                                               getFlightNumber()));

}

public  void  testRemoveFlight()  throws  Exception  {

    //  setup

    FlightDto  expectedFlightDto  =  createARegisteredFlight();

    FlightManagementFacade  facade  =  new  FlightManagementFacadeImpl();

    //  exercise

    facade.removeFlight(expectedFlightDto.getFlightNumber());

    //  verify

    assertFalse("flight  should  not  exist  after  being  removed",

                     facade.flightExists(  expectedFlightDto.

                                                               getFlightNumber()));

}

 

重构说明

Refactoring Notes

我们可以使用“用测试替身替换依赖项” (第522页) 重构将间接输出验证添加到现有测试中。它涉及将代码添加到测试的夹具设置逻辑以创建测试间谍,使用测试间谍需要返回的任何值配置测试间谍,然后安装它。在测试结束时,我们添加断言,将间接输出的预期方法名称和参数与使用检索接口从测试间谍检索到的实际值进行比较。

We can add verification of indirect outputs to existing tests using a Replace Dependency with Test Double (page 522) refactoring. It involves adding code to the fixture setup logic of the tests to create the Test Spy, configuring the Test Spy with any values it needs to return, and installing it. At the end of the test, we add assertions comparing the expected method names and arguments of the indirect outputs with the actual values retrieved from the Test Spy using the Retrieval Interface.

示例:Test Spy

Example: Test Spy

在这个改进的测试版本中,logSpy是我们的测试间谍。 语句使用Setter 注入facade.setAuditLog(logSpy)模式安装测试间谍(请参阅第678页的依赖注入)。 方法是用于访问对记录器调用的实际参数的检索接口getDategetActionCode

In this improved version of the test, logSpy is our Test Spy. The statement facade.setAuditLog(logSpy) installs the Test Spy using the Setter Injection pattern (see Dependency Injection on page 678). The methods getDate, getActionCode, and so on are the Retrieval Interface used to access the actual arguments of the call to the logger.

public void testRemoveFlightLogging_recordingTestStub()

             throws Exception {

    // 固定设置

    FlightDto expectedFlightDto = createAnUnregFlight();

    FlightManagementFacade Facade = new FlightManagementFacadeImpl();

    // 测试替身设置

    AuditLogSpy logSpy = new AuditLogSpy();

    Facade.setAuditLog(logSpy);

    // 练习

    Facade.removeFlight(expectedFlightDto.getFlightNumber());

    // 验证

    assertFalse("删除后航班仍然存在",

                     Facade.flightExists( expectedFlightDto.

                                                            getFlightNumber()));

    assertEquals("呼叫次数", 1,

                        logSpy.getNumberOfCalls());

    assertEquals("操作代码",

                        Helper.REMOVE_FLIGHT_ACTION_CODE,

                        logSpy.getActionCode());

    assertEquals("日期", helper.getTodaysDateWithoutTime(),

                        logSpy.getDate());

    断言Equals("用户", Helper.TEST_USER_NAME,

                        logSpy.getUser());

    断言Equals("详细信息",

                       expectedFlightDto.getFlightNumber(),

                       logSpy.getDetail());

}

public  void  testRemoveFlightLogging_recordingTestStub()

             throws  Exception  {

    //  fixture  setup

    FlightDto  expectedFlightDto  =  createAnUnregFlight();

    FlightManagementFacade  facade  =  new  FlightManagementFacadeImpl();

    //        Test  Double  setup

    AuditLogSpy  logSpy  =  new  AuditLogSpy();

    facade.setAuditLog(logSpy);

    //  exercise

    facade.removeFlight(expectedFlightDto.getFlightNumber());

    //  verify

    assertFalse("flight  still  exists  after  being  removed",

                     facade.flightExists(  expectedFlightDto.

                                                            getFlightNumber()));

    assertEquals("number  of  calls",  1,

                        logSpy.getNumberOfCalls());

    assertEquals("action  code",

                        Helper.REMOVE_FLIGHT_ACTION_CODE,

                        logSpy.getActionCode());

    assertEquals("date",  helper.getTodaysDateWithoutTime(),

                        logSpy.getDate());

    assertEquals("user",  Helper.TEST_USER_NAME,

                        logSpy.getUser());

    assertEquals("detail",

                       expectedFlightDto.getFlightNumber(),

                       logSpy.getDetail());

}

 

此测试依赖于Test Spy的以下定义:

This test depends on the following definition of the Test Spy:

public class AuditLogSpy implements AuditLog {

    // 我们记录实际使用信息的字段

    private Date date;

    private String user;

    private String actionCode;

    private Object detail;

    private int numberOfCalls = 0;

    // 记录真实 AuditLog 接口的实现

    public void logMessage(Date date,

                                           String user,

                                           String actionCode,

                                           Object detail) {

          this.date = date;

          this.user = user;

          this.actionCode = actionCode;

          this.detail = detail;



          numberOfCalls++;

    }



    // 检索接口

    public int getNumberOfCalls() {

          return numberOfCalls;

    }

    public Date getDate() {

          return date;

    }

    public String getUser() {

          return user;

    }

    public String getActionCode() {

          return actionCode;

    }

    public Object getDetail() {

          return detail;

    }

}

public  class  AuditLogSpy  implements  AuditLog  {

    //  Fields  into  which  we  record  actual  usage  information

    private  Date  date;

    private  String  user;

    private  String  actionCode;

    private  Object  detail;

    private  int  numberOfCalls  =  0;

    //  Recording  implementation  of  real  AuditLog  interface

    public  void  logMessage(Date  date,

                                           String  user,

                                           String  actionCode,

                                           Object  detail)  {

          this.date  =  date;

          this.user  =  user;

          this.actionCode  =  actionCode;

          this.detail  =  detail;



          numberOfCalls++;

    }



    //  Retrieval  Interface

    public  int  getNumberOfCalls()  {

          return  numberOfCalls;

    }

    public  Date  getDate()  {

          return  date;

    }

    public  String  getUser()  {

          return  user;

    }

    public  String  getActionCode()  {

          return  actionCode;

    }

    public  Object  getDetail()  {

          return  detail;

    }

}

 

当然,我们可以通过公开间谍的各个字段来实现检索接口,从而避免使用访问器方法。请参阅硬编码测试替身中的示例以了解其他实现选项。

Of course, we could have implemented the Retrieval Interface by making the various fields of our spy public and thereby avoided the need for accessor methods. Please refer to the examples in Hard-Coded Test Double for other implementation options.

模拟对象

Mock Object

我们如何对 SUT 的间接输出实施行为验证?

当逻辑依赖于来自其他软件组件的间接输入时,我们如何独立验证逻辑?

How do we implement Behavior Verification for indirect outputs of the SUT?

How can we verify logic independently when it depends on indirect inputs from other software components?

我们用测试特定的对象替换 SUT 所依赖的对象,以验证 SUT 是否正确使用了该对象。

We replace an object on which the SUT depends on with a test-specific object that verifies it is being used correctly by the SUT.

图像

在许多情况下,SUT 运行的环境或上下文对 SUT 的行为影响很大。在其他情况下,我们必须“深入”SUT 内部,确定是否发生了预期的行为。

In many circumstances, the environment or context in which the SUT operates very much influences the behavior of the SUT. In other cases, we must peer "inside"2 the SUT to determine whether the expected behavior has occurred.

模拟对象是一种强大的方法,可以实现行为验证(第 468页),同时避免类似测试之间的测试代码重复(第 213页)。它的工作原理是将验证 SUT 间接输出的工作完全委托给测试替身(第 522页)。

A Mock Object is a powerful way to implement Behavior Verification (page 468) while avoiding Test Code Duplication (page 213) between similar tests. It works by delegating the job of verifying the indirect outputs of the SUT entirely to a Test Double (page 522).

工作原理

How It Works

首先,我们定义一个模拟对象,该对象实现与 SUT 所依赖的对象相同的接口。然后,在测试期间,我们配置模拟对象,使其具有响应 SUT 的值以及它应该从 SUT 期望的方法调用(包含预期参数)。在执行 SUT 之前,我们安装模拟对象,以便 SUT 使用它而不是实际实现。在 SUT 执行期间调用时,模拟对象使用相等断言将收到的实际参数与预期参数进行比较(请参阅第362页的断言方法),如果它们不匹配,则测试失败。测试根本不需要做出任何断言!

First, we define a Mock Object that implements the same interface as an object on which the SUT depends. Then, during the test, we configure the Mock Object with the values with which it should respond to the SUT and the method calls (complete with expected arguments) it should expect from the SUT. Before exercising the SUT, we install the Mock Object so that the SUT uses it instead of the real implementation. When called during SUT execution, the Mock Object compares the actual arguments received with the expected arguments using Equality Assertions (see Assertion Method on page 362) and fails the test if they don't match. The test need not make any assertions at all!

何时使用它

When to Use It

当我们需要进行行为验证时,可以使用Mock 对象作为观察点,以避免由于无法观察调用 SUT 上方法的副作用而导致的未经测试的需求(请参阅第268页的生产错误)。这种模式通常用于内窥镜测试[ET]或需求驱动开发[MRNO] 。虽然在进行状态验证第 462页)时不需要使用Mock 对象,但我们可能会使用测试桩第 529页)或假对象第 551页)。请注意,测试驱动程序已经发现了Mock 对象工具包的其他用途,但其中许多实际上是使用测试桩而不是Mock 对象的示例。

We can use a Mock Object as an observation point when we need to do Behavior Verification to avoid having an Untested Requirement (see Production Bugs on page 268) caused by our inability to observe the side effects of invoking methods on the SUT. This pattern is commonly used during endoscopic testing [ET] or need-driven development [MRNO]. Although we don't need to use a Mock Object when we are doing State Verification (page 462), we might use a Test Stub (page 529) or Fake Object (page 551). Note that test drivers have found other uses for the Mock Object toolkits, but many of these are actually examples of using a Test Stub rather than a Mock Object.

要使用Mock 对象,我们必须能够在执行 SUT之前预测方法调用的大多数或所有参数的值。如果无法有效地将失败的断言报告回测试运行器(第 377页) ,则不应使用Mock 对象。如果 SUT 在捕获并处理所有异常的容器内运行,则可能就是这种情况。在这种情况下,我们最好改用测试间谍(第 538页)。

To use a Mock Object, we must be able to predict the values of most or all arguments of the method calls before we exercise the SUT. We should not use a Mock Object if a failed assertion cannot be reported back to the Test Runner (page 377) effectively. This may be the case if the SUT runs inside a container that catches and eats all exceptions. In these circumstances, we may be better off using a Test Spy (page 538) instead.

Mock 对象(尤其是使用动态模拟工具创建的模拟对象)经常使用equals被比较的各种对象的方法。如果我们测试特定的相等性与 SUT 的解释不同equals,我们可能无法使用Mock 对象,或者我们可能被迫添加一个equals我们不需要的方法。这种异味称为相等性污染(请参阅217页的生产中的测试逻辑)。一些Mock 对象的实现相等性断言中使用的“比较器”来避免此问题。

Mock Objects (especially those created using dynamic mocking tools) often use the equals methods of the various objects being compared. If our test-specific equality differs from how the SUT would interpret equals, we may not be able to use a Mock Object or we may be forced to add an equals method where we didn't need one. This smell is called Equality Pollution (see Test Logic in Production on page 217). Some implementations of Mock Objects avoid this problem by allowing us to specify the "comparator" to be used in the Equality Assertions.

Mock 对象可以是“严格”的,也可以是“宽松”的(有时也被称为“友好”)。如果接收调用的顺序与Mock 对象编程时指定的顺序不同,则“严格”的Mock 对象将无法通过测试。“宽松”的Mock 对象可以容忍无序调用。

Mock Objects can be either "strict" or "lenient" (sometimes called "nice"). A "strict" Mock Object fails the test if the calls are received in a different order than was specified when the Mock Object was programmed. A "lenient" Mock Object tolerates out-of-order calls.

实施说明

Implementation Notes

使用Mock Objects编写的测试与更传统的测试不同,因为在执行 SUT之前必须指定所有预期行为。这使得测试对于测试自动化新手来说更难编写和理解。这个因素可能足以让我们更喜欢使用Test Spies编写测试。

Tests written using Mock Objects look different from more traditional tests because all the expected behavior must be specified before the SUT is exercised. This makes the tests harder to write and to understand for test automation neophytes. This factor may be enough to cause us to prefer writing our tests using Test Spies.

当我们使用模拟对象时,标准的四阶段测试第 358页)会有所改变具体来说,测试的装置设置阶段被分解为三个特定活动,结果验证阶段或多或少消失了,除了在测试结束时可能存在对“最终验证”方法的调用。

The standard Four-Phase Test (page 358) is altered somewhat when we use Mock Objects. In particular, the fixture setup phase of the test is broken down into three specific activities and the result verification phase more or less disappears, except for the possible presence of a call to the "final verification" method at the end of the test.

夹具设置:

Fixture setup:

练习SUT:

Exercise SUT:

  • SUT 调用Mock Object;Mock Object进行断言。
  • SUT calls Mock Object; Mock Object does assertions.

结果验证:

Result verification:

  • 测试调用“最终验证”方法。
  • Test calls "final verification" method.

夹具拆卸:

Fixture teardown:

  • 没有影响。
  • No impact.

让我们更仔细地研究一下这些差异:

Let's examine these differences a bit more closely:

建造

作为四阶段测试的夹具设置阶段的一部分,我们必须构造用于替换可替代依赖项的Mock 对象。根据我们的编程语言中可用的工具,我们可以手动构建Mock 对象类,使用代码生成器创建Mock 对象类,或者使用动态生成的Mock 对象

As part of the fixture setup phase of our Four-Phase Test, we must construct the Mock Object that we will use to replace the substitutable dependency. Depending on which tools are available in our programming language, we can either build the Mock Object class manually, use a code generator to create a Mock Object class, or use a dynamically generated Mock Object.

具有预期值的配置

由于xUnit 系列中许多成员提供的Mock 对象工具包通常会创建可配置的 Mock 对象(第544页),因此我们需要使用预期的方法调用(及其参数)以及任何函数要返回的值来配置Mock 对象。(某些Mock 对象框架允许我们禁用方法调用验证或仅禁用其参数验证。)我们通常在安装测试替身之前执行此配置。

Because the Mock Object toolkits available in many members of the xUnit family typically create Configurable Mock Objects (page 544), we need to configure the Mock Object with the expected method calls (and their parameters) as well as the values to be returned by any functions. (Some Mock Object frameworks allow us to disable verification of the method calls or just their parameters.) We typically perform this configuration before we install the Test Double.

当我们使用硬编码测试替身(例如内部测试替身)时,不需要此步骤(请参阅硬编码测试替身)。

This step is not needed when we are using a Hard-Coded Test Double such as an Inner Test Double (see Hard-Coded Test Double).

安装

当然,我们必须有一种方式将测试替身安装到 SUT 中,以便能够使用模拟对象。我们可以使用 SUT 支持的任何可替代依赖模式。测试驱动开发社区中的一种常见方法是依赖注入第 678页);更传统的开发人员可能更喜欢依赖查找第 686页)。

Of course, we must have a way of installing a Test Double into the SUT to be able to use a Mock Object. We can use whichever substitutable dependency pattern the SUT supports. A common approach in the test-driven development community is Dependency Injection (page 678); more traditional developers may favor Dependency Lookup (page 686).

用法

当 SUT 调用Mock Object的方法时,这些方法会将方法调用(方法名称加参数)与预期进行比较。如果方法调用是意外的或参数不正确,则断言会立即使测试失败。如果调用是预期的但顺序不对,则严格的Mock Object会立即使测试失败;相反,宽松的Mock Object会注意到调用已收到并继续。调用最终验证方法时会检测到未接的调用。

When the SUT calls the methods of the Mock Object, these methods compare the method call (method name plus arguments) with the expectations. If the method call is unexpected or the arguments are incorrect, the assertion fails the test immediately. If the call is expected but came out of sequence, a strict Mock Object fails the test immediately; by contrast, a lenient Mock Object notes that the call was received and carries on. Missed calls are detected when the final verification method is called.

如果方法调用有任何传出参数或返回值,则Mock 对象需要返回或更新某些内容,以允许 SUT 继续执行测试场景。此行为可以是硬编码的,也可以与预期同时配置。此行为与测试桩相同,只是我们通常会返回满意路径值。

If the method call has any outgoing parameters or return values, the Mock Object needs to return or update something to allow the SUT to continue executing the test scenario. This behavior may be either hard-coded or configured at the same time as the expectations. This behavior is the same as for Test Stubs, except that we typically return happy path values.

最终验证

大多数结果验证发生在Mock 对象内部,因为它被 SUT 调用。如果使用错误的参数调用方法或意外调用方法,则Mock 对象将无法通过测试。但是,如果Mock 对象从未收到预期的方法调用,会发生什么情况?Mock 对象可能无法检测到测试已结束,是时候检查未实现的期望了。因此,我们需要确保调用最终验证方法。一些Mock 对象工具包已经找到了一种通过在方法中包含调用来自动调用此方法的方法tearDown。3许多其他工具包要求我们记住自己调用最终验证方法。

Most of the result verification occurs inside the Mock Object as it is called by the SUT. The Mock Object will fail the test if the methods are called with the wrong arguments or if methods are called unexpectedly. But what happens if the expected method calls are never received by the Mock Object? The Mock Object may have trouble detecting that the test is over and it is time to check for unfulfilled expectations. Therefore, we need to ensure that the final verification method is called. Some Mock Object toolkits have found a way to invoke this method automatically by including the call in the tearDown method.3 Many other toolkits require us to remember to call the final verification method ourselves.

激励人心的例子

Motivating Example

以下测试验证了创建航班的基本功能。但它没有验证 SUT 的间接输出 - 即,SUT 应记录每次创建航班时的信息以及请求者的日期/时间和用户名。

The following test verifies the basic functionality of creating a flight. But it does not verify the indirect outputs of the SUT—namely, the SUT is expected to log each time a flight is created along with the date/time and username of the requester.

public void testRemoveFlight() throws Exception {

    // 设置

    FlightDto expectedFlightDto = createARegisteredFlight();

    FlightManagementFacade Facade = new FlightManagementFacadeImpl();

    // 练习

    Facade.removeFlight(expectedFlightDto.getFlightNumber());

    // 验证

    assertFalse("flight 在被移除后不应存在",

                      Facade.flightExists( expectedFlightDto.

                                                               getFlightNumber()));

}

public  void  testRemoveFlight()  throws  Exception  {

    //  setup

    FlightDto  expectedFlightDto  =  createARegisteredFlight();

    FlightManagementFacade  facade  =  new  FlightManagementFacadeImpl();

    //  exercise

    facade.removeFlight(expectedFlightDto.getFlightNumber());

    //  verify

    assertFalse("flight  should  not  exist  after  being  removed",

                      facade.flightExists(  expectedFlightDto.

                                                               getFlightNumber()));

}

 

重构说明

Refactoring Notes

通过使用“用测试替身替换依赖项”(第522页)重构,可以将间接输出验证添加到现有测试中。这涉及将代码添加到测试的夹具设置逻辑中以创建模拟对象;使用预期的方法调用、参数和要返回的值配置模拟对象;并使用 SUT 提供的任何可替代依赖机制安装它。在测试结束时,如果我们的模拟对象框架需要,我们会添加对最终验证方法的调用。

Verification of indirect outputs can be added to existing tests by using a Replace Dependency with Test Double (page 522) refactoring. This involves adding code to the fixture setup logic of our test to create the Mock Object; configuring the Mock Object with the expected method calls, arguments, and values to be returned; and installing it using whatever substitutable dependency mechanism is provided by the SUT. At the end of the test, we add a call to the final verification method if our Mock Object framework requires one.

示例:模拟对象(手工编码)

Example: Mock Object (Hand-Coded)

在这个改进版本的测试中,mockLog是我们的Mock 对象。该方法setExpectedLogMessage用于使用预期的日志消息对其进行编程。该语句使用Setter 注入(参见依赖注入)测试双重安装模式facade.setAuditLog(mockLog)安装Mock 对象。最后,该方法确保确实进行了对的调用。verify()logMessage()

In this improved version of the test, mockLog is our Mock Object. The method setExpectedLogMessage is used to program it with the expected log message. The statement facade.setAuditLog(mockLog) installs the Mock Object using the Setter Injection (see Dependency Injection) test double-installation pattern. Finally, the verify() method ensures that the call to logMessage() was actually made.

public void testRemoveFlight_Mock() throws Exception {

    // 固定设置

    FlightDto expectedFlightDto = createAnonRegFlight();

    // 模拟配置

    ConfigurableMockAuditLog mockLog =

          new ConfigurableMockAuditLog();

    mockLog.setExpectedLogMessage(

                                          helper.getTodaysDateWithoutTime(),

                                          Helper.TEST_USER_NAME,

                                          Helper.REMOVE_FLIGHT_ACTION_CODE,

                                          expectedFlightDto.getFlightNumber());

    mockLog.setExpectedNumberCalls(1);

    // 模拟安装

    FlightManagementFacade Facade = new FlightManagementFacadeImpl();

    Facade.setAuditLog(mockLog);

    // 练习

    Facade.removeFlight(expectedFlightDto.getFlightNumber());

    // 验证

    assertFalse("flight 在被移除后仍然存在",

                      Facade.flightExists( expectedFlightDto.

                                                               getFlightNumber()));

    mockLog.verify();

}

public  void  testRemoveFlight_Mock()  throws  Exception  {

    //  fixture  setup

    FlightDto  expectedFlightDto  =  createAnonRegFlight();

    //  mock  configuration

    ConfigurableMockAuditLog  mockLog  =

          new  ConfigurableMockAuditLog();

    mockLog.setExpectedLogMessage(

                                          helper.getTodaysDateWithoutTime(),

                                          Helper.TEST_USER_NAME,

                                          Helper.REMOVE_FLIGHT_ACTION_CODE,

                                          expectedFlightDto.getFlightNumber());

    mockLog.setExpectedNumberCalls(1);

    //  mock  installation

    FlightManagementFacade  facade  =  new  FlightManagementFacadeImpl();

    facade.setAuditLog(mockLog);

    //  exercise

    facade.removeFlight(expectedFlightDto.getFlightNumber());

    //  verify

    assertFalse("flight  still  exists  after  being  removed",

                      facade.flightExists(  expectedFlightDto.

                                                               getFlightNumber()));

    mockLog.verify();

}

 

通过使用以下Mock Object ,可以实现此方法。这里我们选择使用手动构建的Mock Object。出于篇幅考虑,仅logMessage显示该方法:

This approach was made possible by use of the following Mock Object. Here we have chosen to use a hand-built Mock Object. In the interest of space, just the logMessage method is shown:

公共无效日志消息(日期实际日期,

                                       字符串实际用户,

                                       字符串实际操作代码,

                                       对象实际细节){

    actualNumberCalls ++;



    Assert.assertEquals(“日期”,expectedDate,actualDate);

    Assert.assertEquals(“用户”,expectedUser,actualUser);

    Assert.assertEquals(“操作代码”,

                                  expectedActionCode,

                                  actualActionCode);

    Assert.assertEquals(“详细信息”,expectedDetail,actualDetail);

}

public  void  logMessage(  Date  actualDate,

                                       String  actualUser,

                                       String  actualActionCode,

                                       Object  actualDetail)  {

    actualNumberCalls++;



    Assert.assertEquals("date",  expectedDate,  actualDate);

    Assert.assertEquals("user",  expectedUser,  actualUser);

    Assert.assertEquals("action  code",

                                  expectedActionCode,

                                  actualActionCode);

    Assert.assertEquals("detail",  expectedDetail,actualDetail);

}

 

断言方法以静态方法的形式调用。在 JUnit 中,这种方法是必需的,因为Mock 对象不是 的子类TestCase;因此它不从 继承断言方法Assert。xUnit 系列的其他成员可能提供不同的机制来访问断言方法。例如,NUnit将它们作为类上的静态方法提供Assert,因此即使测试方法第 348页)也需要以这种方式访问​​断言方法。Ruby 编程语言的 xUnit 系列成员 Test::Unit 将它们作为mixin 提供;因此,它们可以以正常方式调用。

The Assertion Methods are called as static methods. In JUnit, this approach is required because the Mock Object is not a subclass of TestCase; thus it does not inherit the assertion methods from Assert. Other members of the xUnit family may provide different mechanisms to access the Assertion Methods. For example, NUnit provides them only as static methods on the Assert class, so even Test Methods (page 348) need to access the Assertion Methods this way. Test::Unit, the xUnit family member for the Ruby programming language, provides them as mixins; as a consequence, they can be called in the normal fashion.

示例:模拟对象(动态生成)

Example: Mock Object (Dynamically Generated)

最后一个例子使用了手工编码的Mock Object。然而,xUnit 家族的大多数成员都有动态Mock Object框架可用。以下是使用 JMock 重写的相同测试:

The last example used a hand-coded Mock Object. Most members of the xUnit family, however, have dynamic Mock Object frameworks available. Here's the same test rewritten using JMock:

public void testRemoveFlight_JMock() throws Exception {

    // 固定设置

    FlightDto expectedFlightDto = createAnonRegFlight();

    FlightManagementFacade Facade = new FlightManagementFacadeImpl();

    // 模拟配置

    Mock mockLog = mock(AuditLog.class);

    mockLog.expects(once()).method("logMessage")

                   .with(eq(helper.getTodaysDateWithoutTime()),

                           eq(Helper.TEST_USER_NAME),

                           eq(Helper.REMOVE_FLIGHT_ACTION_CODE),

                           eq(expectedFlightDto.getFlightNumber()));

    // 模拟安装

    Facade.setAuditLog((AuditLog) mockLog.proxy());

    // 练习

    Facade.removeFlight(expectedFlightDto.getFlightNumber());

    // 验证

    assertFalse("flight 在被移除后仍然存在",

                      façade.flightExists( expectedFlightDto.

                                                               getFlightNumber()));

    // JMock 自动调用 verify() 方法

}

public  void  testRemoveFlight_JMock()  throws  Exception  {

    //  fixture  setup

    FlightDto  expectedFlightDto  =  createAnonRegFlight();

    FlightManagementFacade  facade  =  new  FlightManagementFacadeImpl();

    //  mock  configuration

    Mock  mockLog  =  mock(AuditLog.class);

    mockLog.expects(once()).method("logMessage")

                   .with(eq(helper.getTodaysDateWithoutTime()),

                           eq(Helper.TEST_USER_NAME),

                           eq(Helper.REMOVE_FLIGHT_ACTION_CODE),

                           eq(expectedFlightDto.getFlightNumber()));

    //  mock  installation

    facade.setAuditLog((AuditLog)  mockLog.proxy());

    //  exercise

    facade.removeFlight(expectedFlightDto.getFlightNumber());

    //  verify

    assertFalse("flight  still  exists  after  being  removed",

                      facade.flightExists(  expectedFlightDto.

                                                               getFlightNumber()));

    //  verify()  method  called  automatically  by  JMock

}

 

请注意 JMock 如何提供“流畅”的配置接口(请参阅可配置测试替身),使我们能够以相当易读的方式指定预期的方法调用。JMock 还允许我们指定断言要使用的比较器;在本例中,调用会导致调用eq默认方法。equals

Note how JMock provides a "fluent" Configuration Interface (see Configurable Test Double) that allows us to specify the expected method calls in a fairly readable fashion. JMock also allows us to specify the comparator to be used by the assertions; in this case, the calls to eq cause the default equals method to be called.

进一步阅读

几乎每本关于使用 xUnit 进行自动化测试的书都提到了Mock Objects,所以我不会在这里列出这些资源。在阅读其他书籍时,请记住Mock Object一词通常用于指代测试桩,有时甚至指代Fake Objects。Mocks 、Fakes、Stubs 和 Dummies (附录 B中)对各种书籍和文章中使用的术语进行了更全面的比较。

Almost every book on automated testing using xUnit has something to say about Mock Objects, so I won't list those resources here. As you are reading other books, keep in mind that the term Mock Object is often used to refer to a Test Stub and sometimes even to Fake Objects. Mocks, Fakes, Stubs, and Dummies (in Appendix B) contains a more thorough comparison of the terminology used in various books and articles.

假物体

Fake Object

当依赖对象无法使用时,如何独立验证逻辑?

如何避免慢测试?

How can we verify logic independently when depended-on objects cannot be used?

How can we avoid Slow Tests?

我们用更轻量的实现替换了 SUT 所依赖的组件。

We replace a component that the SUT depends on with a much lighter-weight implementation.

也称为

Also known as

假的

Dummy

图像

SUT 通常依赖于其他组件或系统。尽管与其他组件的交互可能是必要的,但由实际 DOC 实现的这些交互的副作用可能是不必要的,甚至是有害的。

The SUT often depends on other components or systems. Although the interactions with these other components may be necessary, the side effects of these interactions as implemented by the real DOC may be unnecessary or even detrimental.

虚假对象是DOC 提供的功能的更简单、更轻量的实现,并且没有我们选择不做的副作用。

A Fake Object is a much simpler and lighter-weight implementation of the functionality provided by the DOC without the side effects we choose to do without.

工作原理

How It Works

我们获取或构建一个非常轻量级的实现,该实现具有 SUT 所依赖的组件所提供的相同功能,并指示 SUT 使用它而不是真正的 DOC。此实现不需要具有真正的 DOC 所需的任何“功能”(例如可扩展性);它只需向 SUT 提供等效服务,这样 SUT 就不会意识到它没有使用真正的 DOC。

We acquire or build a very lightweight implementation of the same functionality as provided by a component on which the SUT depends and instruct the SUT to use it instead of the real DOC. This implementation need not have any of the "-ilities" that the real DOC needs to have (such as scalability); it need provide only the equivalent services to the SUT so that the SUT remains unaware it isn't using the real DOC.

虚假对象是一种测试替身(第 522页),它与测试桩(第 529页) 在许多方面相似,包括需要在 SUT 中安装可替代的依赖项。测试桩充当控制点,将间接输入注入 SUT,而虚假对象则不然:它仅提供一种以自洽方式进行交互的方法。这些交互(即 SUT 和虚假对象之间的交互)通常有很多,并且作为先前方法调用的参数传入的值通常会作为后续方法调用的结果返回。将此行为与测试桩模拟对象(第 544页) 的行为进行对比,其中响应是硬编码的或由测试配置的。

A Fake Object is a kind of Test Double (page 522) that is similar to a Test Stub (page 529) in many ways, including the need to install into the SUT a substitutable dependency. Whereas a Test Stub acts as a control point to inject indirect inputs into the SUT, however, the Fake Object does not: It merely provides a way for the interactions to occur in a self-consistent manner. These interactions (i.e., between the SUT and the Fake Object) will typically be many, and the values passed in as arguments of earlier method calls will often be returned as results of later method calls. Contrast this behavior with that of Test Stubs and Mock Objects (page 544), where the responses are either hard-coded or configured by the test.

虽然测试通常不会配置虚假对象,但复杂的夹具设置(通常涉及初始化 DOC 的状态)也可以直接使用后门操作(第327页)对虚假对象进行设置。数据加载器(请参阅后门操作)和后门设置(请参阅后门操作)等技术可以非常成功地使用,而不必担心过度指定的软件(请参阅第 239页的脆弱测试),因为它们只是将我们绑定到 SUT 和虚假对象之间的接口;用于配置虚假对象的接口是仅供测试关注的。

While the test does not normally configure a Fake Object, complex fixture setup that would typically involve initializing the state of the DOC may also be done with the Fake Object directly using Back Door Manipulation (page 327). Techniques such as Data Loader (see Back Door Manipulation) and Back Door Setup (see Back Door Manipulation) can be used quite successfully with less fear of Overspecified Software (see Fragile Test on page 239) because they simply bind us to the interface between the SUT and the Fake Object; the interface used to configure the Fake Object is a test-only concern.

何时使用它

When to Use It

当 SUT 依赖于其他不可用的组件或使测试变得困难或缓慢(例如,慢速测试;参见第 253页)且测试需要比在测试桩模拟对象中实现更复杂的行为序列时,我们应该使用伪对象。如果构建伪对象值得,那么创建轻量级实现也必须比构建和编写合适的模拟对象更容易,至少从长远来看是这样。

We should use a Fake Object whenever the SUT depends on other components that are unavailable or that make testing difficult or slow (e.g., Slow Tests; see page 253) and the tests need more complex sequences of behavior than are worth implementing in a Test Stub or Mock Object. It must also be easier to create a lightweight implementation than to build and program suitable Mock Objects, at least in the long run, if building a Fake Object is to be worthwhile.

使用伪对象有助于我们避免过度指定软件,因为我们不会在测试中对 DOC 的确切调用序列进行编码。SUT 可以改变 DOC 方法的调用次数,而不会导致测试失败。

Using a Fake Object helps us avoid Overspecified Software because we do not encode the exact calling sequences expected of the DOC within the test. The SUT can vary how many times the methods of the DOC are called without causing tests to fail.

如果我们需要控制间接输入或者验证 SUT 的间接输出,我们可能应该使用Mock ObjectTest Stub

If we need to control the indirect inputs or verify the indirect outputs of the SUT, we should probably use a Mock Object or Test Stub instead.

接下来介绍一些我们用假对象替换真实组件的具体情况。

Some specific situations where we replace the real component with a Fake Object are described next.

变体:假数据库

使用伪数据库模式,真正的数据库或持久层将被功能相同但性能更佳的伪对象所取代。我们经常使用的方法是用一组内存中的对象替换数据库,HashTable这些对象是一种非常轻量级的方式,用于检索测试早期已“持久化”的对象。

With the Fake Database pattern, the real database or persistence layer is replaced by a Fake Object that is functionally equivalent but that has much better performance characteristics. An approach we have often used involves replacing the database with a set of in-memory HashTables that act as a very lightweight way of retrieving objects that have been "persisted" earlier in the test.

变体:内存数据库

伪对象的另一个示例是使用占用空间小的无磁盘数据库,而不是功能齐全的基于磁盘的数据库。这种内存数据库将至少将测试速度提高一个数量级,同时放弃的功能比伪数据库要少。

Another example of a Fake Object is the use of a small-footprint, diskless database instead of a full-featured disk-based database. This kind of In-Memory Database will improve the speed of tests by at least an order of magnitude while giving up less functionality than a Fake Database.

变体:虚假 Web 服务

当测试依赖于作为 Web 服务访问的其他组件的软件时,我们可以构建一个小型的硬编码或数据驱动的实现,用它来代替真正的 Web 服务,从而使我们的测试更加健壮,并避免必须在我们的开发环境中创建真正的 Web 服务的测试实例。

When testing software that depends on other components that are accessed as Web services, we can build a small hard-coded or data-driven implementation that can be used instead of the real Web service to make our tests more robust and to avoid having to create a test instance of the real Web service in our development environment.

变体:伪服务层

在测试用户界面时,我们可以将实现应用程序服务层[PEAA] (包括域层)的组件替换为返回记忆或数据驱动结果的伪对象,从而避免测试的数据敏感性(参见脆弱性测试和行为敏感性(参见脆弱性测试)。这种方法使我们能够专注于测试用户界面,而不必担心返回的数据会随时间而变化。

When testing user interfaces, we can avoid Data Sensitivity (see Fragile Test) and Behavior Sensitivity (see Fragile Test) of the tests by replacing the component that implements the Service Layer [PEAA] (including the domain layer) of our application with a Fake Object that returns remembered or data-driven results. This approach allows us to focus on testing the user interface without having to worry about the data being returned changing over time.

实施说明

Implementation Notes

引入虚假对象涉及两个基本问题:

Introducing a Fake Object involves two basic concerns:

构建虚假对象

大多数伪对象都是手工构建的。通常,伪对象用于用更轻量的内存实现来替换由于实际消息传递或磁盘 I/O 而遭受延迟问题的实际实现。借助大多数面向对象编程语言中提供的丰富类库,通常可以构建一个足以满足 SUT 需求的伪实现,至少对于特定测试而言,而且工作量相对较少。

Most Fake Objects are hand-built. Often, the Fake Object is used to replace a real implementation that suffers from latency issues owing to real messaging or disk I/O with a much lighter in-memory implementation. With the rich class libraries available in most object-oriented programming languages, it is usually possible to build a fake implementation that is sufficient to satisfy the needs of the SUT, at least for the purposes of specific tests, with relatively little effort.

一种流行的策略是首先构建一个伪对象来支持一组特定的测试,其中 SUT 只需要 DOC 服务的一个子集。如果这证明是成功的,我们可能会考虑扩展伪对象来处理其他测试。随着时间的推移,我们可能会发现我们可以使用伪对象运行所有测试。(请参阅第319页的侧栏“不使用共享装置进行更快的测试”,了解我们如何使用哈希表伪造整个数据库并使我们的测试运行速度提高 50 倍。)

A popular strategy is to start by building a Fake Object to support a specific set of tests where the SUT requires only a subset of the DOC's services. If this proves successful, we may consider expanding the Fake Object to handle additional tests. Over time, we may find that we can run all of our tests using the Fake Object. (See the sidebar "Faster Tests Without Shared Fixtures" on page 319 for a description of how we faked out the entire database with hash tables and made our tests run 50 times faster.)

安装虚假对象

当然,我们必须有一种将伪对象安装到 SUT 中的方法才能利用它。我们可以使用 SUT 支持的任何可替代依赖模式。测试驱动开发社区中的一种常见方法是依赖注入第 678页);更传统的开发人员可能更喜欢依赖查找第 686页)。当我们引入伪数据库(请参阅第551页的伪对象)以加快客户测试的执行速度时,后一种技术也更合适;依赖注入对于这些类型的测试效果不太好。

Of course, we must have a way of installing the Fake Object into the SUT to be able to take advantage of it. We can use whichever substitutable dependency pattern the SUT supports. A common approach in the test-driven development community is Dependency Injection (page 678); more traditional developers may favor Dependency Lookup (page 686). The latter technique is also more appropriate when we introduce a Fake Database (see Fake Object on page 551) in an effort to speed up execution of the customer tests; Dependency Injection doesn't work so well with these kinds of tests.

激励人心的例子

Motivating Example

在此示例中,SUT 需要从数据库中读取和写入记录。测试必须在数据库中设置夹具(多次写入),SUT 与数据库进行多次交互(读取和写入),然后测试从数据库中删除记录(多次删除)。所有这些工作都需要时间——每个测试需要几秒钟。这很快就会累积到几分钟,很快我们发现我们的开发人员并没有如此频繁地运行测试。以下是其中一个测试的示例:

In this example, the SUT needs to read and write records from a database. The test must set up the fixture in the database (several writes), the SUT interacts (reads and writes) with the database several more times, and then the test removes the records from the database (several deletes). All of this work takes time—several seconds per test. This very quickly adds up to minutes, and soon we find that our developers aren't running the tests quite so frequently. Here is an example of one of these tests:

public void testReadWrite() throws Exception{

    // 设置

    FlightMngtFacade Facade = new FlightMgmtFacadeImpl();

    BigDecimal yyc = Facade.createAirport("YYC", "Calgary", "Calgary");

    BigDecimal lax = Facade.createAirport("LAX", "LAX Intl", "LA");

    Facade.createFlight(yyc, lax);

    // 练习

    列表 flights = Facade.getFlightsByOriginAirport(yyc);

    // 验证

    assertEquals( "# of flights", 1, flights.size());

    Flight flight = (Flight) flights.get(0);

    assertEquals( "origin",

                         yyc, flight.getOrigin().getCode());

}

public  void  testReadWrite()  throws  Exception{

    //  Setup

    FlightMngtFacade  facade  =  new  FlightMgmtFacadeImpl();

    BigDecimal  yyc  =  facade.createAirport("YYC",  "Calgary",  "Calgary");

    BigDecimal  lax  =  facade.createAirport("LAX",  "LAX  Intl",  "LA");

    facade.createFlight(yyc,  lax);

    //  Exercise

    List  flights  =  facade.getFlightsByOriginAirport(yyc);

    //  Verify

    assertEquals(  "#  of  flights",  1,  flights.size());

    Flight  flight  =  (Flight)  flights.get(0);

    assertEquals(  "origin",

                         yyc,  flight.getOrigin().getCode());

}

 

测试调用createAirport我们的服务外观 [CJ2EEP] 它调用我们的数据访问层等。下面是我们调用的几个方法的实际实现:

The test calls createAirport on our Service Facade [CJ2EEP], which calls, among other things, our data access layer. Here is the actual implementation of several of the methods we are calling:

public BigDecimal createAirport( String airportCode,

                                                 String name,

                                                 String vicinityCity)

throws FlightBookingException{

    TransactionManager.beginTransaction();

    Airport airport = dataAccess.

            createAirport(airportCode, name, vicinityCity);

    logMessage("错误的操作代码", airport.getCode());//bug

    TransactionManager.commitTransaction();

    return airport.getId();

}



public List getFlightsByOriginAirport(

                         BigDecimal originAirportId)

       throws FlightBookingException {



    if (originAirportId == null)

         throw new InvalidArgumentException(

                       "未提供出发地机场 ID",

                       "originAirportId", null);

    Airport origin = dataAccess.getAirportByPrimaryKey(originAirportId);

    List flights = dataAccess.getFlightsByOriginAirport(origin);



    return flights;

}

public  BigDecimal  createAirport(  String  airportCode,

                                                 String  name,

                                                 String  nearbyCity)

throws  FlightBookingException{

    TransactionManager.beginTransaction();

    Airport  airport  =  dataAccess.

            createAirport(airportCode,  name,  nearbyCity);

    logMessage("Wrong  Action  Code",  airport.getCode());//bug

    TransactionManager.commitTransaction();

    return  airport.getId();

}



public  List  getFlightsByOriginAirport(

                         BigDecimal  originAirportId)

       throws  FlightBookingException  {



    if  (originAirportId  ==  null)

         throw  new  InvalidArgumentException(

                       "Origin  Airport  Id  has  not  been  provided",

                       "originAirportId",  null);

    Airport  origin  =  dataAccess.getAirportByPrimaryKey(originAirportId);

    List  flights  =  dataAccess.getFlightsByOriginAirport(origin);



    return  flights;

}

 

dataAccess.createAirport对、dataAccess.createFlight和 的调用TransactionManager.commitTransaction导致我们的测试速度最慢。对dataAccess.getAirportByPrimaryKey和 的调用dataAccess.getFlightsByOriginAirport是次要因素,但仍然导致测试速度变慢。

The calls to dataAccess.createAirport, dataAccess.createFlight, and TransactionManager.commitTransaction cause our test to slow down the most. The calls to dataAccess.getAirportByPrimaryKey and dataAccess.getFlightsByOriginAirport are a lesser factor but still contribute to the slow test.

重构说明

Refactoring Notes

引入Fake Object的步骤与添加Mock Object的步骤非常相似。如果不存在,我们使用 Replace Dependency with Test Double(第522页)重构来引入一种用Fake Object替换DOC 的方法——通常是一个字段(属性)来保存对它的引用。在静态类型语言中,我们可能必须先进行 Extract Interface [Fowler] 重构,然后才能引入伪实现。然后,我们使用这个接口作为保存对可替换依赖项的引用的变量类型。

The steps for introducing a Fake Object are very similar to those for adding a Mock Object. If one doesn't already exist, we use a Replace Dependency with Test Double (page 522) refactoring to introduce a way to substitute the Fake Object for the DOC—usually a field (attribute) to hold the reference to it. In statically typed languages, we may have to do an Extract Interface [Fowler] refactoring before we can introduce the fake implementation. Then, we use this interface as the type of variable that holds the reference to the substitutable dependency.

一个显著的区别是,我们不需要为Fake Object配置期望值或返回值;我们只需以正常方式设置装置。

One notable difference is that we do not need to configure the Fake Object with expectations or return values; we merely set up the fixture in the normal way.

示例:虚假数据库

Example: Fake Database

在此示例中,我们创建了一个替代数据库的Fake Object ,即完全使用哈希表在内存中实现的Fake Database。测试不会发生很大变化,但测试执行速度会快得多。

In this example, we've created a Fake Object that replaces the database—that is, a Fake Database implemented entirely in memory using hash tables. The test doesn't change a lot, but the test execution occurs much, much faster.

public void testReadWrite_inMemory() throws Exception{

    // 设置

    FlightMgmtFacadeImpl Facade = new FlightMgmtFacadeImpl();

    Facade.setDao(new InMemoryDatabase());

    BigDecimal yyc = Facade.createAirport("YYC", "Calgary", "Calgary");

    BigDecimal lax = Facade.createAirport("LAX", "LAX Intl", "LA");

    Facade.createFlight(yyc, lax);

    // 练习

    列表 flights = Facade.getFlightsByOriginAirport(yyc);

    // 验证

    assertEquals( "# of flights", 1, flights.size());

    Flight flight = (Flight) flights.get(0);

    assertEquals( "origin",

                        yyc, flight.getOrigin().getCode());

}

public  void  testReadWrite_inMemory()  throws  Exception{

    //  Setup

    FlightMgmtFacadeImpl  facade  =  new  FlightMgmtFacadeImpl();

    facade.setDao(new  InMemoryDatabase());

    BigDecimal  yyc  =  facade.  createAirport("YYC",  "Calgary",  "Calgary");

    BigDecimal  lax  =  facade.  createAirport("LAX",  "LAX  Intl",  "LA");

    facade.createFlight(yyc,  lax);

    //  Exercise

    List  flights  =  facade.getFlightsByOriginAirport(yyc);

    //  Verify

    assertEquals(  "#  of  flights",  1,  flights.size());

    Flight  flight  =  (Flight)  flights.get(0);

    assertEquals(  "origin",

                        yyc,  flight.getOrigin().getCode());

}

 

以下是假数据库的实现

Here's the implementation of the Fake Database:

公共类 InMemoryDatabase 实现 FlightDao{

    私有列表 airports = new Vector();

    公共 Airport createAirport(String airportCode,

                                                String name, String vicinityCity)

                  抛出 DataException, InvalidArgumentException {

        assertParamtersAreValid( airportCode, name, vicinityCity);

        assertAirportDoesntExist( airportCode);

        Airport result = new Airport(getNextAirportId(),

                 airportCode, name, createCity(nearbyCity));

        airports.add(result);

        返回结果;

  }

公共 Airport getAirportByPrimaryKey(BigDecimal airportId)

              抛出 DataException, InvalidArgumentException {

    assertAirportNotNull(airportId);



    Airport result = null;

    迭代器 i = airports.iterator();

    while (i.hasNext()) {

          Airport airport = (Airport) i.next();

          if (airport.getId().equals(airportId)) {

             返回机场;

          }

    }

    抛出新的DataException(“未找到机场:”+airportId);

}

public  class  InMemoryDatabase  implements  FlightDao{

    private  List  airports  =  new  Vector();

    public  Airport  createAirport(String  airportCode,

                                                String  name,  String  nearbyCity)

                  throws  DataException,  InvalidArgumentException  {

        assertParamtersAreValid(    airportCode,  name,  nearbyCity);

        assertAirportDoesntExist(  airportCode);

        Airport  result  =  new  Airport(getNextAirportId(),

                 airportCode,  name,  createCity(nearbyCity));

        airports.add(result);

        return  result;

  }

public  Airport  getAirportByPrimaryKey(BigDecimal  airportId)

              throws  DataException,  InvalidArgumentException  {

    assertAirportNotNull(airportId);



    Airport  result  =  null;

    Iterator  i  =  airports.iterator();

    while  (i.hasNext())  {

          Airport  airport  =  (Airport)  i.next();

          if  (airport.getId().equals(airportId))  {

             return  airport;

          }

    }

    throw  new  DataException("Airport  not  found:"+airportId);

}

 

现在我们所需要的只是将虚假数据库安装到外观中的方法的实现,以便我们的开发人员在每次更改代码后都乐意运行所有测试。

Now all we need is the implementation of the method that installs the Fake Database into the facade to make our developers more than happy to run all the tests after every code change.

公共无效setDao(FlightDao){

    dataAccess = dao;

}

public  void  setDao(FlightDao)  {

    dataAccess  =  dao;

}

 
进一步阅读

第 319页的侧栏“不使用共享装置,测试速度更快”更深入地描述了我们如何使用哈希表伪造整个数据库,并使测试运行速度提高 50 倍。Mocks 、Fakes、Stubs 和 Dummies (附录 B中)对各种书籍和文章中使用的术语进行了更全面的比较。

The sidebar "Faster Tests Without Shared Fixtures" on page 319 provides a more in-depth description of how we faked out the entire database with hash tables and made our tests run 50 times faster. Mocks, Fakes, Stubs, and Dummies (in Appendix B) contains a more thorough comparison of the terminology used in various books and articles.

可配置测试替身

Configurable Test Double

也称为

Also known as

可配置的模拟对象、可配置的测试间谍、可配置的测试桩

Configurable Mock Object, Configurable Test Spy, Configurable Test Stub

我们如何告诉测试替身返回什么或者期待什么?

How do we tell a Test Double what to return or expect?

我们配置一个可重复使用的测试替身,其中包含在测试的夹具设置阶段要返回或验证的值。

We configure a reusable Test Double with the values to be returned or verified during the fixture setup phase of a test.

图像

有些测试需要将唯一值作为间接输入输入到 SUT 中,或作为 SUT 的间接输出进行验证。这种方法通常需要使用测试替身(第 522页) 作为测试和 SUT 之间的管道;同时,需要以某种方式告知测试替身要返回或验证哪些值。

Some tests require unique values to be fed into the SUT as indirect inputs or to be verified as indirect outputs of the SUT. This approach typically requires the use of Test Doubles (page 522) as the conduit between the test and the SUT; at the same time, the Test Double somehow needs to be told which values to return or verify.

可配置测试替身是一种通过在多个测试中重复使用测试替身来减少测试代码重复第 213页)的方法。其使用的关键是配置测试替身在运行时要返回或预期的值。

A Configurable Test Double is a way to reduce Test Code Duplication (page 213) by reusing a Test Double in many tests. The key to its use is to configure the Test Double's values to be returned or expected at runtime.

工作原理

How It Works

测试替身由实例变量构成,这些实例变量保存要返回给 SUT 的值或用作方法调用参数的预期值。测试在测试设置阶段通过调用测试替身接口上的相应方法来初始化这些变量。当 SUT 调用测试替身上的方法时,测试替身会使用相应变量的内容作为要返回的值或断言中的预期值。

The Test Double is built with instance variables that hold the values to be returned to the SUT or to serve as the expected values of arguments to method calls. The test initializes these variables during the setup phase of the test by calling the appropriate methods on the Test Double's interface. When the SUT calls the methods on the Test Double, the Test Double uses the contents of the appropriate variable as the value to return or as the expected value in assertions.

何时使用它

When to Use It

当我们需要在依赖于测试替身的多个测试中实现相似但略有不同的行为,并且希望避免测试代码重复模糊测试第 186页)时,可以使用可配置测试替身。在后一种情况下,我们需要在读取测试时查看测试替身使用的值。如果我们只希望使用一次测试替身,并且不值得花费额外的精力和构建可配置测试替身的复杂性,我们可以考虑使用硬编码测试替身第 568页) 。

We can use a Configurable Test Double whenever we need similar but slightly different behavior in several tests that depend on Test Doubles and we want to avoid Test Code Duplication or Obscure Tests (page 186)—in the latter case, we need to see what values the Test Double is using as we read the test. If we expect only a single usage of a Test Double, we can consider using a Hard-Coded Test Double (page 568) if the extra effort and complexity of building a Configurable Test Double are not warranted.

实施说明

Implementation Notes

测试替身可配置测试替身,因为它需要为测试提供一种方法来配置它,使其具有要返回的值和/或要期望的方法参数。可配置测试桩(第529页)和测试间谍(第538页)只需要一种方法来配置对其方法调用的响应;可配置模拟对象(第544页)还需要一种方法来配置它们的期望(调用哪些方法以及使用哪些参数)。

A Test Double is a Configurable Test Double because it needs to provide a way for the tests to configure it with values to return and/or method arguments to expect. Configurable Test Stubs (page 529) and Test Spies (page 538) simply require a way to configure the responses to calls on their methods; configurable Mock Objects (page 544) also require a way to configure their expectations (which methods should be called and with which arguments).

可配置测试替身可以通过多种方式构建。确定具体实现方式需要做出两个相对独立的决定:(1) 如何配置可配置测试替身;(2) 如何编码可配置测试替身。

Configurable Test Doubles may be built in many ways. Deciding on a particular implementation involves making two relatively independent decisions: (1) how the Configurable Test Double will be configured and (2) how the Configurable Test Double will be coded.

有两种常见的方法来配置可配置测试替身。最流行的方法是提供一个配置接口,该接口仅供测试使用,以配置要作为间接输入返回的值以及间接输出的预期值。或者,我们可以用两种模式构建可配置测试替身配置模式在夹具设置期间使用,通过使用预期参数调用可配置测试替身的方法来安装间接输入和预期间接输出。在安装可配置测试替身之前,它会进入正常(“使用”或“回放”)模式。

There are two common ways to configure a Configurable Test Double. The most popular approach is to provide a Configuration Interface that is used only by the test to configure the values to be returned as indirect inputs and the expected values of the indirect outputs. Alternatively, we may build the Configurable Test Double with two modes. The Configuration Mode is used during fixture setup to install the indirect inputs and expected indirect outputs by calling the methods of the Configurable Test Double with the expected arguments. Before the Configurable Test Double is installed, it is put into the normal ("usage" or "playback") mode.

构建可配置测试替身的明显方法是创建手工构建的测试替身。但是,如果我们足够幸运,有人已经为我们构建了可配置测试替身生成工具。测试替身生成器有两种类型:代码生成器和在运行时构造对象的工具。开发人员已经构建了几代“模拟”工具,其中几个已经移植到其他编程语言;请访问http://xprogramming.com以查看您选择的编程语言中有哪些可用工具。如果答案是“没有”,您可以自己手工编写测试替身,尽管这确实需要更多努力。

The obvious way to build a Configurable Test Double is to create a Hand-Built Test Double. If we are lucky, however, someone will have already built a tool to generate a Configurable Test Double for us. Test Double generators come in two flavors: code generators and tools that fabricate the object at runtime. Developers have built several generations of "mocking" tools, and several of these have been ported to other programming languages; check out http://xprogramming.com to see what is available in your programming language of choice. If the answer is "nothing," you can hand-code the Test Double yourself, although this does take somewhat more effort.

变体:配置界面

配置接口包含一组单独的方法,可配置测试替身专门提供这些方法供测试使用,以设置可配置测试替身返回或期望接收的每个值。测试只需在四阶段测试(第 358页) 的夹具设置阶段调用这些方法。SUT 使用可配置测试替身上的“其他”方法(“常规”接口)。它不知道配置接口存在于它委托的对象上。

A Configuration Interface comprises a separate set of methods that the Configurable Test Double provides specifically for use by the test to set each value that the Configurable Test Double returns or expects to receive. The test simply calls these methods during the fixture setup phase of the Four-Phase Test (page 358). The SUT uses the "other" methods on the Configurable Test Double (the "normal" interface). It isn't aware that the Configuration Interface exists on the object to which it is delegating.

配置接口有两种类型。早期的工具包(例如 MockMaker)为我们需要配置的每个值生成一个不同的方法。这些 setter 方法的集合构成了配置接口。最近推出的工具包(例如 JMock)提供了一个通用接口,用于构建可配置测试替身在运行时解释的预期行为规范(请参阅第468页的行为验证)。设计良好的流畅接口可以使测试更易于阅读和理解。

Configuration Interfaces come in two flavors. Early toolkits, such as MockMaker, generated a distinct method for each value we needed to configure. The collection of these setter methods made up the Configuration Interface. More recently introduced toolkits, such as JMock, provide a generic interface that is used to build an Expected Behavior Specification (see Behavior Verification on page 468) that the Configurable Test Double interprets at runtime. A well-designed fluent interface can make the test much easier to read and understand.

变体:配置模式

我们可以通过提供配置模式来避免定义一组单独的方法来配置测试替身,测试将使用该配置模式“教导”可配置测试替身应期望的内容。乍一看,这种配置测试替身的方法可能会令人困惑:为什么测试方法(第 348页) 在调用它在 SUT 上执行的方法之前会调用这个其他对象的方法?当我们意识到我们正在进行一种“记录和回放”的形式时,这种技术就更有意义了。

We can avoid defining a separate set of methods to configure the Test Double by providing a Configuration Mode that the test uses to "teach" the Configurable Test Double what to expect. At first glance, this means of configuring the Test Double can be confusing: Why does the Test Method (page 348) call the methods of this other object before it calls the methods it is exercising on the SUT? When we come to grips with the fact that we are doing a form of "record and playback," this technique makes a bit more sense.

使用配置模式的主要优点是,它避免了创建一组单独的方法来配置可配置测试替身,因为我们重用了 SUT 将要调用的相同方法。(我们必须提供一种方法来设置方法要返回的值,因此我们至少要添加一个额外的方法。)另一方面,SUT 预计要调用的每种方法现在都有两条代码路径:一条用于配置模式,另一条用于“使用模式”。

The main advantage of using a Configuration Mode is that it avoids creating a separate set of methods for configuring the Configurable Test Double because we reuse the same methods that the SUT will be calling. (We do have to provide a way to set the values to be returned by the methods, so we have at least one additional method to add.) On the flip side, each method that the SUT is expected to call now has two code paths through it: one for the Configuration Mode and another for the "usage mode."

变体:手工制作的测试替身

手工构建的测试替身是由测试自动化程序为一个或多个特定测试定义的测试替身。硬编码测试替身本质上是手工构建的测试替身,而可配置测试替身可以是手工构建的,也可以是生成的。本书在很多示例中都使用了手工构建的测试替身,因为当我们拥有实际、简单、具体的代码时,更容易了解正在发生的事情。这是使用手工构建的测试替身的主要优势;事实上,有些人认为这个好处非常重要,以至于他们只使用手工构建的测试替身。在没有第三方工具包可用或项目或公司政策禁止我们使用这些工具时,我们也可能使用手工构建的测试替身。

A Hand-Built Test Double is one that was defined by the test automater for one or more specific tests. A Hard-Coded Test Double is inherently a Hand-Built Test Double, while a Configurable Test Double can be either hand-built or generated. This book uses Hand-Built Test Doubles in a lot of the examples because it is easier to see what is going on when we have actual, simple, concrete code to look at. This is the main advantage of using a Hand-Built Test Double; indeed, some people consider this benefit to be so important that they use Hand-Built Test Doubles exclusively. We may also use a Hand-Built Test Double when no third-party toolkits are available or if we are prevented from using those tools by project or corporate policy.

变体:静态生成测试替身

早期的第三方工具包使用代码生成器来创建静态生成的测试替身的代码。然后编译代码并将其与我们手写的测试代码链接起来。通常,我们会将代码存储在源代码存储库 [SCM] 中。当然,每当目标类的接口发生变化时,我们都必须重新生成静态生成的测试替身的代码。将此步骤作为自动构建脚本的一部分可能会很有优势,以确保每当接口发生变化时它确实会发生。

The early third-party toolkits used code generators to create the code for Statically Generated Test Doubles. The code is then compiled and linked with our handwritten test code. Typically, we will store the code in a source code repository [SCM]. Whenever the interface of the target class changes, of course, we must regenerate the code for our Statically Generated Test Doubles. It may be advantageous to include this step as part of the automated build script to ensure that it really does happen whenever the interface changes.

实例化静态生成的测试替身与实例化手工构建的测试替身相同也就是说,我们使用生成的类的名称来构造可配置测试替身

Instantiating a Statically Generated Test Double is the same as instantiating a Hand-Built Test Double. That is, we use the name of the generated class to construct the Configurable Test Double.

重构过程中会出现一个有趣的问题。假设我们通过向其中一个方法添加参数来更改要替换的类的接口。那么我们应该重构生成的代码吗?还是应该在重构它所替换的代码后重新生成静态生成的测试替身?使用现代重构工具,似乎可以一步完成重构生成的代码和使用它的测试;然而,这种策略可能会导致静态生成的测试替身没有参数验证逻辑或新参数的变量。因此,我们应该在重构完成后重新生成静态生成的测试替身,以确保重构后的静态生成的测试替身正常工作并且可以由代码生成器重新创建。

An interesting problem arises during refactoring. Suppose we change the interface of the class we are replacing by adding an argument to one of the methods. Should we then refactor the generated code? Or should we regenerate the Statically Generated Test Double after the code it replaces has been refactored? With modern refactoring tools, it may seem easier to refactor the generated code and the tests that use it in a single step; this strategy, however, may leave the Statically Generated Test Double without argument verification logic or variables for the new parameter. Therefore, we should regenerate the Statically Generated Test Double after the refactoring is finished to ensure that the refactored Statically Generated Test Double works properly and can be recreated by the code generator.

变体:动态生成测试替身

较新的第三方工具包在运行时生成可配置测试替身,方法是使用编程语言的反射功能检查类或接口并构建能够理解对其方法的所有调用的对象。这些可配置测试替身可能会在运行时解释行为规范,也可能生成可执行代码;尽管如此,我们无需生成和维护或重新生成源代码。缺点就是没有代码可看——但除非我们特别多疑或偏执,否则这真的不算缺点。

Newer third-party toolkits generate Configurable Test Doubles at runtime by using the reflection capabilities of the programming language to examine a class or interface and build an object that is capable of understanding all calls to its methods. These Configurable Test Doubles may interpret the behavior specification at runtime or they may generate executable code; nevertheless, there is no source code for us to generate and maintain or regenerate. The down side is simply that there is no code to look at—but that really isn't a disadvantage unless we are particularly suspicious or paranoid.

当今的大多数工具都会生成Mock 对象,因为它们是最流行和使用最广泛的选项。但是,我们仍然可以将这些对象用作测试桩,因为它们确实提供了一种在调用特定方法时设置要返回的值的方法。如果我们对验证所调用的方法或传递给它们的参数不是特别感兴趣,大多数工具包都提供了一种指定“不关心”参数的方法。鉴于大多数工具包都会生成Mock 对象,它们通常不提供检索接口(请参阅测试间谍)。

Most of today's tools generate Mock Objects because they are the most fashionable and widely used options. We can still use these objects as Test Stubs, however, because they do provide a way of setting the value to be returned when a particular method is called. If we aren't particularly interested in verifying the methods being called or the arguments passed to them, most toolkits provide a way to specify "don't care" arguments. Given that most toolkits generate Mock Objects, they typically don't provide a Retrieval Interface (see Test Spy).

激励人心的例子

Motivating Example

这是一个使用硬编码测试替身来控制时间的测试:

Here's a test that uses a Hard-Coded Test Double to give it control over the time:

public void testDisplayCurrentTime_AtMidnight_HCM()

              throws Exception {

    // Fixture Setup

    // 实例化硬编码测试桩:

    TimeProvider testStub = new MidnightTimeProvider();

    // 实例化 SUT

    TimeDisplay sut = new TimeDisplay();

    // 将桩注入 SUT

    sut.setTimeProvider(testStub);

    // 练习 SUT

    String result = sut.getCurrentTimeAsHtmlFragment();

    // 验证直接输出

    字符串 expectedTimeString =

          "<span class=\"tinyBoldText\">Midnight</span>";

    assertEquals("Midnight", expectedTimeString, result);

}

public  void  testDisplayCurrentTime_AtMidnight_HCM()

              throws  Exception  {

    //  Fixture  Setup

    //      Instantiate  hard-code  Test  Stub:

    TimeProvider  testStub  =  new  MidnightTimeProvider();

    //      Instantiate  SUT

    TimeDisplay  sut  =  new  TimeDisplay();

    //      Inject  Stub  into  SUT

    sut.setTimeProvider(testStub);

    //  Exercise  SUT

    String  result  =  sut.getCurrentTimeAsHtmlFragment();

    //  Verify  Direct  Output

    String  expectedTimeString  =

          "<span  class=\"tinyBoldText\">Midnight</span>";

    assertEquals("Midnight",  expectedTimeString,  result);

}

 

如果不了解硬编码测试替身的定义,这个测试很难理解。很容易看出,如果定义不明确,这种不清晰会导致神秘嘉宾(参见模糊测试)。

This test is hard to understand without seeing the definition of the Hard-Coded Test Double. It is easy to see how this lack of clarity can lead to a Mystery Guest (see Obscure Test) if the definition is not close at hand.

类 MidnightTimeProvider 实现 TimeProvider {

    public Calendar getTime() {

        Calendar myTime = new GregorianCalendar();

        myTime.set(Calendar.HOUR_OF_DAY, 0);

        myTime.set(Calendar.MINUTE, 0);

        返回 myTime;

    }

}

class  MidnightTimeProvider  implements  TimeProvider  {

    public  Calendar  getTime()  {

        Calendar  myTime  =  new  GregorianCalendar();

        myTime.set(Calendar.HOUR_OF_DAY,  0);

        myTime.set(Calendar.MINUTE,  0);

        return  myTime;

    }

}

 

我们可以通过使用自我分流(参见硬编码测试替身)来解决模糊测试问题,以使硬编码测试替身在测试中可见:

We can solve the Obscure Test problem by using a Self Shunt (see Hard-Coded Test Double) to make the Hard-Coded Test Double visible within the test:

public class SelfShuntExample extends TestCase

implements TimeProvider {

    public void testDisplayCurrentTime_AtMidnight() throws Exception {

        // Fixture Setup

        TimeDisplay sut = new TimeDisplay();

        // Mock Setup

        sut.setTimeProvider(this); // self shunt installation

        // Exercise SUT

        String result = sut.getCurrentTimeAsHtmlFragment();

        // Verify Direct Output

        String expectedTimeString =

              "<span class=\"tinyBoldText\">Midnight</span>";

        assertEquals("Midnight", expectedTimeString, result);

    }



    public Calendar getTime() {

        Calendar myTime = new GregorianCalendar();

        myTime.set(Calendar.MINUTE, 0);

        myTime.set(Calendar.HOUR_OF_DAY, 0);

        return myTime;

    }

}

public  class  SelfShuntExample  extends  TestCase

implements  TimeProvider  {

    public  void  testDisplayCurrentTime_AtMidnight()  throws  Exception  {

        //  Fixture  Setup

        TimeDisplay  sut  =  new  TimeDisplay();

        //  Mock  Setup

        sut.setTimeProvider(this);  //  self  shunt  installation

        //  Exercise  SUT

        String  result  =  sut.getCurrentTimeAsHtmlFragment();

        //  Verify  Direct  Output

        String  expectedTimeString  =

              "<span  class=\"tinyBoldText\">Midnight</span>";

        assertEquals("Midnight",  expectedTimeString,  result);

    }



    public  Calendar  getTime()  {

        Calendar  myTime  =  new  GregorianCalendar();

        myTime.set(Calendar.MINUTE,  0);

        myTime.set(Calendar.HOUR_OF_DAY,  0);

        return  myTime;

    }

}

 

不幸的是,我们需要在每个需要它的测试用例类(第 373页) 中构建测试替身行为,这会导致测试代码重复

Unfortunately, we will need to build the Test Double behavior into each Testcase Class (page 373) that requires it, which results in Test Code Duplication.

重构说明

Refactoring Notes

将使用硬编码测试替身的测试重构为使用第三方可配置测试替身的测试相对简单。我们只需按照工具包提供的说明实例化可配置测试替身,并使用与硬编码测试替身中相同的值对其进行配置。我们可能还必须将测试替身中最初硬编码的一些逻辑移到测试方法中,并将其作为配置步骤的一部分传递给测试替身。

Refactoring a test that uses a Hard-Coded Test Double to become a test that uses a third-party Configurable Test Double is relatively straightforward. We simply follow the directions provided with the toolkit to instantiate the Configurable Test Double and configure it with the same values as we used in the Hard-Coded Test Double. We may also have to move some of the logic that was originally hard-coded within the Test Double into the Test Method and pass it in to the Test Double as part of the configuration step.

将实际的硬编码测试替身转换为可配置测试替身稍微复杂一些,但如果我们只需要捕获简单的行为,那么也不会太复杂。 (对于更复杂的行为,我们最好研究现有的工具包之一,并将其​​移植到我们的环境中(如果尚不可用)。)首先,我们需要引入一种方法来设置要返回或预期的值。最好的选择是从修改测试开始,看看我们想要如何与可配置测试替身交互。在测试的夹具设置部分期间实例化它之后,我们使用新出现的配置接口配置模式将特定于测试的值传递给可配置测试替身。一旦我们知道了如何使用可配置测试替身,我们就可以使用引入字段 [JetBrains] 重构来创建可配置测试替身的实例变量来保存每个先前的硬编码值。

Converting the actual Hard-Coded Test Double into a Configurable Test Double is a bit more complicated, but not overly so if we need to capture only simple behavior. (For more complex behavior, we're probably better off examining one of the existing toolkits and porting it to our environment if it is not yet available.) First we need to introduce a way to set the values to be returned or expected. The best choice is to start by modifying the test to see how we want to interact with the Configurable Test Double. After instantiating it during the fixture setup part of the test, we then pass the test-specific values to the Configurable Test Double using the emerging Configuration Interface or Configuration Mode. Once we've seen how we want to use the Configurable Test Double, we can use an Introduce Field [JetBrains] refactoring to create the instance variables of the Configurable Test Double to hold each of the previously hard-coded values.

示例:使用 Setter 的配置接口

Example: Configuration Interface Using Setters

以下示例展示了测试如何使用 Setter 注入来使用简单的手工构建的配置接口

The following example shows how a test would use a simple hand-built Configuration Interface using Setter Injection:

public void testDisplayCurrentTime_AtMidnight()

                    throws Exception {

      // Fixture 设置

      // 测试替身配置

      TimeProviderTestStub tpStub = new TimeProviderTestStub();

      tpStub.setHours(0);

      tpStub.setMinutes(0);

      // 实例化 SUT

      TimeDisplay sut = new TimeDisplay();

      // 测试替身安装

      sut.setTimeProvider(tpStub);

      // 练习 SUT

      String result = sut.getCurrentTimeAsHtmlFragment();

      // 验证结果

      String expectedTimeString =

                  "<span class=\"tinyBoldText\">Midnight</span>";

      assertEquals("Midnight", expectedTimeString, result);

}

public  void  testDisplayCurrentTime_AtMidnight()

                    throws  Exception  {

      //  Fixture  setup

      //        Test  Double  configuration

      TimeProviderTestStub  tpStub  =  new  TimeProviderTestStub();

      tpStub.setHours(0);

      tpStub.setMinutes(0);

      //      Instantiate  SUT

      TimeDisplay  sut  =  new  TimeDisplay();

      //            Test  Double  installation

      sut.setTimeProvider(tpStub);

      //  Exercise  SUT

      String  result  =  sut.getCurrentTimeAsHtmlFragment();

      //  Verify  Outcome

      String  expectedTimeString  =

                  "<span  class=\"tinyBoldText\">Midnight</span>";

      assertEquals("Midnight",  expectedTimeString,  result);

}

 

可配置测试替身实现如下:

The Configurable Test Double is implemented as follows:

class TimeProviderTestStub implements TimeProvider {

    // 配置接口

    public void setHours(int hours) {

        // 0 表示午夜;12 表示中午

        myTime.set(Calendar.HOUR_OF_DAY, hours);

    }

    public void setMinutes(int minutes) {

        myTime.set(Calendar.MINUTE, minutes);

    }

    // SUT 使用的接口

    public Calendar getTime() {

        // @return 上次设置的时间

        return myTime;

    }

}

class  TimeProviderTestStub  implements  TimeProvider  {

    //  Configuration  Interface

    public  void  setHours(int  hours)  {

        //  0  is  midnight;  12  is  noon

        myTime.set(Calendar.HOUR_OF_DAY,  hours);

    }

    public  void  setMinutes(int  minutes)  {

        myTime.set(Calendar.MINUTE,  minutes);

    }

    //  Interface  Used  by  SUT

    public  Calendar  getTime()  {

        //  @return  the  last  time  that  was  set

        return  myTime;

    }

}

 

示例:使用表达式生成器的配置界面

Example: Configuration Interface Using Expression Builder

现在让我们将上例中定义的配置接口与 JMock 框架提供的配置接口进行对比。JMock 动态生成模拟对象,并提供通用的流畅接口,以意图揭示的方式配置模拟对象。以下是转换为使用 JMock 的相同测试:

Now let's contrast the Configuration Interface we defined in the previous example with the one provided by the JMock framework. JMock generates Mock Objects dynamically and provides a generic fluent interface for configuring the Mock Object in an intent-revealing style. Here's the same test converted to use JMock:

public void testDisplayCurrentTime_AtMidnight_JM()

          throws Exception {

    // Fixture 设置

    TimeDisplay sut = new TimeDisplay();

    // 测试替身配置

    Mock tpStub = mock(TimeProvider.class);

    Calendar midnight = makeTime(0,0);

    tpStub.stubs().method("getTime").

                        withNoArguments().

                        will(returnValue(midnight));

    // 测试替身安装

    sut.setTimeProvider((TimeProvider) tpStub);

    // 练习 SUT

    String result = sut.getCurrentTimeAsHtmlFragment();

    // 验证结果

    String expectedTimeString =

               "<span class=\"tinyBoldText\">Midnight</span>";

    assertEquals("Midnight", expectedTimeString, result);

}

public  void  testDisplayCurrentTime_AtMidnight_JM()

          throws  Exception  {

    //  Fixture  setup

    TimeDisplay  sut  =  new  TimeDisplay();

    //    Test  Double  configuration

    Mock  tpStub  =  mock(TimeProvider.class);

    Calendar  midnight  =  makeTime(0,0);

    tpStub.stubs().method("getTime").

                        withNoArguments().

                        will(returnValue(midnight));

    //    Test  Double  installation

    sut.setTimeProvider((TimeProvider)  tpStub);

    //  Exercise  SUT

    String  result  =  sut.getCurrentTimeAsHtmlFragment();

    //  Verify  Outcome

    String  expectedTimeString  =

               "<span  class=\"tinyBoldText\">Midnight</span>";

    assertEquals("Midnight",  expectedTimeString,  result);

}

 

这里我们将一些构造要返回时间的逻辑移到了Testcase 类中,因为在通用模拟框架中没有办法做到这一点;我们使用了测试实用程序方法第 599页)来构造要返回的时间。下一个示例显示了一个可配置的模拟对象,其中包含多个预期参数:

Here we have moved some of the logic to construct the time to be returned into the Testcase Class because there is no way to do it in the generic mocking framework; we've used a Test Utility Method (page 599) to construct the time to be returned. This next example shows a configurable Mock Object complete with multiple expected parameters:

public void testRemoveFlight_JMock() throws Exception {

    // 固定设置

    FlightDto expectedFlightDto = createAnonRegFlight();

    FlightManagementFacade Facade = new FlightManagementFacadeImpl();

    // 模拟配置

    Mock mockLog = mock(AuditLog.class);

    mockLog.expects(once()).method("logMessage")

                    .with(eq(helper.getTodaysDateWithoutTime()),

                            eq(Helper.TEST_USER_NAME),

                            eq(Helper.REMOVE_FLIGHT_ACTION_CODE),

                            eq(expectedFlightDto.getFlightNumber()));

    // 模拟安装

    Facade.setAuditLog((AuditLog) mockLog.proxy());

    // 练习

    Facade.removeFlight(expectedFlightDto.getFlightNumber());

    // 验证

    assertFalse("flight 在被移除后仍然存在",

                      façade.flightExists( expectedFlightDto.

                                                               getFlightNumber()));

    // JMock 自动调用 verify() 方法

}

public  void  testRemoveFlight_JMock()  throws  Exception  {

    //  fixture  setup

    FlightDto  expectedFlightDto  =  createAnonRegFlight();

    FlightManagementFacade  facade  =  new  FlightManagementFacadeImpl();

    //  mock  configuration

    Mock  mockLog  =  mock(AuditLog.class);

    mockLog.expects(once()).method("logMessage")

                    .with(eq(helper.getTodaysDateWithoutTime()),

                            eq(Helper.TEST_USER_NAME),

                            eq(Helper.REMOVE_FLIGHT_ACTION_CODE),

                            eq(expectedFlightDto.getFlightNumber()));

    //  mock  installation

    facade.setAuditLog((AuditLog)  mockLog.proxy());

    //  exercise

    facade.removeFlight(expectedFlightDto.getFlightNumber());

    //  verify

    assertFalse("flight  still  exists  after  being  removed",

                      facade.flightExists(  expectedFlightDto.

                                                               getFlightNumber()));

    //  verify()  method  called  automatically  by  JMock

}

 

预期行为规范是通过调用表达式构建方法(如expectsonce和 )来构建的method,以描述应如何使用可配置测试替身以及应返回什么。与我们手工构建的可配置测试替身相比,JMock 支持更复杂的行为规范(例如使用不同的参数和返回值多次调用同一方法)。

The Expected Behavior Specification is built by calling expression-building methods such as expects, once, and method to describe how the Configurable Test Double should be used and what it should return. JMock supports the specification of much more sophisticated behavior (such as multiple calls to the same method with different arguments and return values) than does our hand-built Configurable Test Double.

示例:配置模式

Example: Configuration Mode

在下一个示例中,测试已转换为使用具有配置模式的模拟对象

In the next example, the test has been converted to use a Mock Object with a Configuration Mode:

public void testRemoveFlight_ModalMock() throws Exception {

    // 固定设置

    FlightDto expectedFlightDto = createAnonRegFlight();

    // 模拟配置(在配置模式下)

    ModalMockAuditLog mockLog = new ModalMockAuditLog();

    mockLog.logMessage(Helper.getTodaysDateWithoutTime(),

                                      Helper.TEST_USER_NAME,

                                      Helper.REMOVE_FLIGHT_ACTION_CODE,

                                     expectedFlightDto.getFlightNumber());

    mockLog.enterPlaybackMode();

    // 模拟安装

    FlightManagementFacade Facade = new FlightManagementFacadeImpl();

    Facade.setAuditLog(mockLog);

    // 练习

    Facade.removeFlight(expectedFlightDto.getFlightNumber());

    // 验证

    assertFalse("flight 在被移除后仍然存在",

                     Facade.flightExists( expectedFlightDto.

                                                              getFlightNumber()));

    mockLog.verify();

}

public  void  testRemoveFlight_ModalMock()  throws  Exception  {

    //  fixture  setup

    FlightDto  expectedFlightDto  =  createAnonRegFlight();

    //  mock  configuration  (in  Configuration  Mode)

    ModalMockAuditLog  mockLog  =  new  ModalMockAuditLog();

    mockLog.logMessage(Helper.getTodaysDateWithoutTime(),

                                      Helper.TEST_USER_NAME,

                                      Helper.REMOVE_FLIGHT_ACTION_CODE,

                                     expectedFlightDto.getFlightNumber());

    mockLog.enterPlaybackMode();

    //  mock  installation

    FlightManagementFacade  facade  =  new  FlightManagementFacadeImpl();

    facade.setAuditLog(mockLog);

    //  exercise

    facade.removeFlight(expectedFlightDto.getFlightNumber());

    //  verify

    assertFalse("flight  still  exists  after  being  removed",

                     facade.flightExists(  expectedFlightDto.

                                                              getFlightNumber()));

    mockLog.verify();

}

 

在这里,测试在夹具设置阶段调用可配置测试替身上的方法。如果我们不知道此测试使用了可配置测试替身模拟,乍一看,我们可能会觉得这个结构令人困惑。最明显的线索是调用方法enterPlaybackMode,它告诉可配置测试替身停止保存预期值并开始对其进行断言。

Here the test calls the methods on the Configurable Test Double during the fixture setup phase. If we weren't aware that this test uses a Configurable Test Double mock, we might find this structure confusing at first glance. The most obvious clue to its intent is the call to the method enterPlaybackMode, which tells the Configurable Test Double to stop saving expected values and to start asserting on them.

本测试使用的可配置测试替身实现如下:

The Configurable Test Double used by this test is implemented like this:

private int mode = record;

public void enterPlaybackMode() {

      mode = playing;

}



public void logMessage( Date date,

                                        String user,

                                        String action,

                                        Object detail) {

    if (mode == record) {

        Assert.assertEquals("仅支持 1 次预期呼叫",

                                            0, expectedNumberCalls);

        expectedNumberCalls = 1;

        expectedDate = date;

        expectedUser = user;

        expectedCode = action;

        expectedDetail = detail;

    } else {

        Assert.assertEquals("Date", expectedDate, date);

        Assert.assertEquals("User", expectedUser, user);

        Assert.assertEquals("Action", expectedCode, action);

        Assert.assertEquals("Detail", expectedDetail, detail);

    }

}

private  int  mode  =  record;

public  void  enterPlaybackMode()  {

      mode  =  playback;

}



public  void  logMessage(  Date  date,

                                        String  user,

                                        String  action,

                                        Object  detail)  {

    if  (mode  ==  record)  {

        Assert.assertEquals("Only  supports  1  expected  call",

                                            0,  expectedNumberCalls);

        expectedNumberCalls  =  1;

        expectedDate  =  date;

        expectedUser  =  user;

        expectedCode  =  action;

        expectedDetail  =  detail;

    }  else  {

        Assert.assertEquals("Date",  expectedDate,  date);

        Assert.assertEquals("User",  expectedUser,  user);

        Assert.assertEquals("Action",  expectedCode,  action);

        Assert.assertEquals("Detail",  expectedDetail,  detail);

    }

}

 

if语句检查我们处于录制模式还是回放模式。由于这个简单的手工构建的可配置测试替身只允许存储一个值,因此如果Guard Assertion(第490页)试图记录对此方法的多次调用,则测试失败。该子句的其余部分将参数保存到变量中,并将其用作子句中相等性断言(请参阅第362页的断言方法then的预期值。else

The if statement checks whether we are in record or playback mode. Because this simple hand-built Configurable Test Double allows only a single value to be stored, a Guard Assertion (page 490) fails the test if it tries to record more than one call to this method. The rest of the then clause saves the parameters into variables that it uses as the expected values of the Equality Assertions (see Assertion Method on page 362) in the else clause.

硬编码测试替身

Hard-Coded Test Double

也称为

Also known as

硬编码模拟对象、硬编码测试桩、硬编码测试间谍

Hard-Coded Mock Object, Hard-Coded Test Stub, Hard-Coded Test Spy

我们如何告诉测试替身返回什么或者期待什么?

How do we tell a Test Double what to return or expect?

我们通过硬编码返回值和/或预期调用来构建测试替身。

We build the Test Double by hard-coding the return values and/or expected calls.

图像

在开发全自动测试(见第 26页)的过程中,出于多种原因会使用测试替身(第522)。测试替身的行为可能因测试而异,并且有多种方法可以定义此行为。

Test Doubles (page 522) are used for many reasons during the development of Fully Automated Tests (see page 26). The behavior of the Test Double may vary from test to test, and there are many ways to define this behavior.

测试替身非常简单或者特定于单个测试时,最简单的解决方案通常是将行为硬编码到测试替身中。

When the Test Double is very simple or very specific to a single test, the simplest solution is often to hard-code the behavior into the Test Double.

工作原理

How It Works

测试自动化程序将测试替身的所有行为硬编码到测试替身中。例如,如果测试替身需要为方法调用返回一个值,则该值将被硬编码到返回语句中。如果它需要验证某个参数是否具有特定值,则断言将使用预期值进行硬编码。

The test automater hard-codes all of the Test Double's behavior into the Test Double. For example, if the Test Double needs to return a value for a method call, the value is hard-coded into the return statement. If it needs to verify that a certain parameter had a specific value, the assertion is hard-coded with the value that is expected.

何时使用它

When to Use It

当测试替身的行为非常简单或特定于单个测试或测试用例类(第373页)时,我们通常会使用硬编码测试替身。硬编码测试替身可以是测试桩(第529页)、测试间谍(第 538页) 或模拟对象(第544页),具体取决于我们在 SUT 调用的方法中编码的内容。

We typically use a Hard-Coded Test Double when the behavior of the Test Double is very simple or is very specific to a single test or Testcase Class (page 373). The Hard-Coded Test Double can be either a Test Stub (page 529), a Test Spy (page 538), or a Mock Object (page 544), depending on what we encode in the method(s) called by the SUT.

由于每个硬编码测试替身都是专门手工构建的,因此其构建可能比使用第三方可配置测试替身(第 558页) 花费更多精力。随着 SUT 的变化,它还可能导致需要维护和重构更多测试代码。如果不同的测试要求测试替身以不同的方式运行,而使用硬编码测试替身会导致过多的测试代码重复(第213页),我们应该考虑改用可配置测试替身

Because each Hard-Coded Test Double is purpose-built by hand, its construction may take more effort than using a third-party Configurable Test Double (page 558). It can also result in more test code to maintain and refactor as the SUT changes. If different tests require that the Test Double behave in different ways and the use of Hard-Coded Test Doubles results in too much Test Code Duplication (page 213), we should consider using a Configurable Test Double instead.

实施说明

Implementation Notes

硬编码测试替身本质上是手工构建的测试替身(参见可配置测试替身),因为自动生成硬编码测试替身往往没有什么意义。硬编码测试替身可以用专门的类来实现,但它们最常用于编程语言支持块、闭包内部类的情况。所有这些语言特性都有助于避免创建硬编码测试替身所产生的文件 / 类开销;它们还能让硬编码测试替身的行为在使用它的测试中保持可见。在某些语言中,这会使测试更难阅读。当我们使用匿名内部类时尤其如此,因为匿名内部类需要大量的语法开销来内联定义类。在直接支持块且开发人员非常熟悉其使用习惯的语言中,使用硬编码测试替身实际上可以使测试更易读。

Hard-Coded Test Doubles are inherently Hand-Built Test Doubles (see Configurable Test Double) because there tends to be no point in generating Hard-Coded Test Doubles automatically. Hard-Coded Test Doubles can be implemented with dedicated classes, but they are most commonly used when the programming language supports blocks, closures, or inner classes. All of these language features help to avoid the file/class overhead associated with creating a Hard-Coded Test Double; they also keep the Hard-Coded Test Double's behavior visible within the test that uses it. In some languages, this can make the tests a bit more difficult to read. This is especially true when we use anonymous inner classes, which require a lot of syntactic overhead to define the class in-line. In languages that support blocks directly, and in which developers are very familiar with their usage idioms, using Hard-Coded Test Doubles can actually make the tests easier to read.

实现硬编码测试替身的方法有很多种每种方法都有各自的优点和缺点。

There are many different ways to implement a Hard-Coded Test Double, each of which has its own advantages and disadvantages.

变体:测试替身类

我们可以将硬编码测试替身实现为不同于测试用例类或 SUT 的类。这样,硬编码测试替身就可以被多个测试用例类重用,但可能会导致模糊测试第 186页;由神秘来宾引起),因为它会将 SUT 的重要间接输入或间接输出从测试移到其他地方,可能不在测试阅读者的视线范围内。根据我们实现测试替身类的方式,它还可能导致代码激增和需要维护额外的测试替身类。

We can implement the Hard-Coded Test Double as a class distinct from either the Testcase Class or the SUT. This allows the Hard-Coded Test Double to be reused by several Testcase Classes but may result in an Obscure Test (page 186; caused by a Mystery Guest) because it moves important indirect inputs or indirect outputs of the SUT out of the test to somewhere else, possibly out of sight of the test reader. Depending on how we implement the Test Double Class, it may also result in code proliferation and additional Test Double classes to maintain.

确保测试替身类与它将要替换的组件类型兼容的一种方法是使测试替身类成为该组件的子类。然后,我们重写任何我们想要更改其行为的方法。

One way to ensure that the Test Double Class is type-compatible with the component it will replace is to make the Test Double Class a subclass of that component. We then override any methods whose behavior we want to change.

变体:测试替身子类

我们还可以通过子类化真实 DOC 并重写我们期望 SUT 在执行时调用的方法的行为来实现硬编码测试替身。不幸的是,如果 SUT 调用我们未重写的其他 DOC 方法,这种方法可能会产生不可预测的后果。它还将我们的测试代码与 DOC 的实现紧密联系在一起,并可能导致过度指定的软件(请参阅第239页的脆弱测试)。在非常特殊的情况下(例如,在进行尖峰测试时或当它是我们唯一可用的选项时),使用测试替身子类可能是一个合理的选择,但不建议在日常工作中采用这种策略。

We can also implement the Hard-Coded Test Double by subclassing the real DOC and overriding the behavior of the methods we expect the SUT to call as we exercise it. Unfortunately, this approach can have unpredictable consequences if the SUT calls other DOC methods that we have not overridden. It also ties our test code very closely to the implementation of the DOC and can result in Overspecified Software (see Fragile Test on page 239). Using a Test Double Subclass may be a reasonable option in very specific circumstances (e.g., while doing a spike or when it is the only option available to us), but this strategy isn't recommended on a routine basis.

变体:自分流

也称为

Also known as

环回,测试用例类作为测试替身

Loopback, Testcase Class as Test Double

我们可以在测试用例类上实现我们希望 SUT 调用的方法,并将测试用例对象第 382页)安装到 SUT 中作为要使用的测试替身。这种方法称为自分流

We can implement the methods that we want the SUT to call on the Testcase Class and install the Testcase Object (page 382) into the SUT as the Test Double to be used. This approach is called a Self Shunt.

Self Shunt可以是测试桩测试间谍模拟对象,具体取决于 SUT 调用的方法。在每种情况下,它都需要访问测试用例类的实例变量才能知道要做什么或期望什么。在静态类型语言中,测试用例类还必须实现 SUT 所依赖的接口。

The Self Shunt can be either a Test Stub, a Test Spy, or a Mock Object, depending on what the method called by the SUT does. In each case, it will need to access instance variables of the Testcase Class to know what to do or expect. In statically typed languages, the Testcase Class must also implement the interface on which the SUT depends.

当我们需要一个特定于单个测试用例类的硬编码测试替身时,我们通常会使用自分流。如果只有一个测试方法第 348页)需要硬编码测试替身并且我们的语言支持,那么使用内部测试替身可能会带来更大的清晰度。

We typically use a Self Shunt when we need a Hard-Coded Test Double that is very specific to a single Testcase Class. If only a single Test Method (page 348) requires the Hard-Coded Test Double, using an Inner Test Double may result in greater clarity if our language supports it.

变体:内部测试替身

实现硬编码测试替身的常用方法是将其编码为测试方法中的匿名内部类或块闭包。此策略使测试替身可以访问测试用例类的实例变量和常量,甚至测试方法的局部变量,从而无需配置测试替身

A popular way to implement a Hard-Coded Test Double is to code it as an anonymous inner class or block closure within the Test Method. This strategy gives the Test Double access to instance variables and constants of the Testcase Class and even the local variables of the Test Method, which can eliminate the need to configure the Test Double.

虽然此变体的名称基于它所利用的 Java 语言构造的名称,但许多编程语言都有等效的机制,用于定义稍后使用块或闭包运行的代码。

While the name of this variation is based on the name of the Java language construct of which it takes advantage, many programming languages have an equivalent mechanism for defining code to be run later using blocks or closures.

我们通常在构建相对简单且仅在单个测试方法中使用的硬编码测试替身时使用内部测试替身。许多人发现使用硬编码测试替身比使用自分流器更直观,因为他们可以准确地看到测试方法中发生的情况。然而,不熟悉匿名内部类或块语法的读者可能会发现测试难以理解。

We typically use an Inner Test Double when we are building a Hard-Coded Test Double that is relatively simple and is used only within a single Test Method. Many people find the use of a Hard-Coded Test Double more intuitive than using a Self Shunt because they can see exactly what is going on within the Test Method. Readers who are unfamiliar with the syntax of anonymous inner classes or blocks may find the test difficult to understand, however.

变体:伪对象

硬编码测试替身的编写者面临的一个挑战是,我们必须实现 SUT可能调用的接口中的所有方法。在 Java 和 C# 等静态类型语言中,我们必须至少实现与我们访问 DOC 相关的类或类型所隐含的接口中声明的所有方法。这通常会“迫使”我们从实际 DOC 中子类化,以避免为这些方法提供虚拟实现。

One challenge facing writers of Hard-Coded Test Doubles is that we must implement all the methods in the interface that the SUT might call. In statically typed languages such as Java and C#, we must at least implement all methods declared in the interface implied by the class or type associated with however we access the DOC. This often "forces" us to subclass from the real DOC to avoid providing dummy implementations for these methods.

减少编程工作量的一种方法是提供一个默认类,该类实现所有接口方法并抛出唯一错误。然后,我们可以通过子类化这个具体类并重写我们期望 SUT 在执行时调用的一种方法来实现硬编码测试替身。如果 SUT 调用任何其他方法,伪对象就会抛出错误,从而导致测试失败。

One way of reducing the programming effort is to provide a default class that implements all the interface methods and throws a unique error. We can then implement a Hard-Coded Test Double by subclassing this concrete class and overriding just the one method we expect the SUT to call while we are exercising it. If the SUT calls any other methods, the Pseudo-Object throws an error, thereby failing the test.

激励人心的例子

Motivating Example

以下测试验证了组件的基本功能,该组件格式化包含当前时间的 HTML 字符串。不幸的是,它依赖于真实的系统时钟,因此很少能通过!

The following test verifies the basic functionality of the component that formats an HTML string containing the current time. Unfortunately, it depends on the real system clock, so it rarely passes!

public void testDisplayCurrentTime_AtMidnight() {

    // 固定设置

    TimeDisplay sut = new TimeDisplay();

    // 练习 SUT

    String result = sut.getCurrentTimeAsHtmlFragment();

    // 验证直接输出

    String expectedTimeString =

             "<span class=\"tinyBoldText\">Midnight</span>";

    assertEquals( expectedTimeString, result);

}

public  void  testDisplayCurrentTime_AtMidnight()  {

    //  fixture  setup

    TimeDisplay  sut  =  new  TimeDisplay();

    //  exercise  SUT

    String  result  =  sut.getCurrentTimeAsHtmlFragment();

    //  verify  direct  output

    String  expectedTimeString  =

             "<span  class=\"tinyBoldText\">Midnight</span>";

    assertEquals(  expectedTimeString,  result);

}

 

重构说明

Refactoring Notes

最常见的转换是从使用真实组件到使用硬编码测试替身。4要进行这种转换,我们需要构建测试替身本身并从我们的测试方法中安装它。如果 SUT 尚不支持此安装,我们可能还需要引入一种使用依赖注入模式之一(第 678页)安装测试替身的方法。执行此操作的过程在用测试替身替换依赖项(第 522页)重构中进行了描述。

The most common transition is from using the real component to using a Hard-Coded Test Double.4 To make this transition, we need to build the Test Double itself and install it from within our Test Method. We may also need to introduce a way to install the Test Double using one of the Dependency Injection patterns (page 678) if the SUT does not already support this installation. The process for doing so is described in the Replace Dependency with Test Double (page 522) refactoring.

示例:测试替身类

Example: Test Double Class

以下是经过修改的相同测试,使用硬编码测试替身类来控制时间:

Here's the same test modified to use a Hard-Coded Test Double class to allow control over the time:

public void testDisplayCurrentTime_AtMidnight_HCM()

              throws Exception {

    // Fixture 设置

    // 实例化硬编码测试桩

    TimeProvider testStub = new MidnightTimeProvider();

    // 实例化 SUT

    TimeDisplay sut = new TimeDisplay();

    // 将测试桩注入 SUT

    sut.setTimeProvider(testStub);

    // 练习 SUT

    String result = sut.getCurrentTimeAsHtmlFragment();

    // 验证直接输出

    String expectedTimeString =

        "<span class=\"tinyBoldText\">Midnight</span>";

    assertEquals("Midnight", expectedTimeString, result);

}

public  void  testDisplayCurrentTime_AtMidnight_HCM()

              throws  Exception  {

    //  Fixture  setup

    //      Instantiate  hard-coded  Test  Stub

    TimeProvider  testStub  =  new  MidnightTimeProvider();

    //      Instantiate  SUT

    TimeDisplay  sut  =  new  TimeDisplay();

    //      Inject  Test  Stub  into  SUT

    sut.setTimeProvider(testStub);

    //  Exercise  SUT

    String  result  =  sut.getCurrentTimeAsHtmlFragment();

    //  Verify  direct  output

    String  expectedTimeString  =

        "<span  class=\"tinyBoldText\">Midnight</span>";

    assertEquals("Midnight",  expectedTimeString,  result);

}

 

如果不了解硬编码测试替身的定义,这个测试很难理解。我们可以很容易地看到,如果硬编码测试替身不在附近,这种方法可能会导致由神秘嘉宾引起的模糊测试。

This test is hard to understand without seeing the definition of the Hard-Coded Test Double. We can readily see how this approach might lead to an Obscure Test caused by a Mystery Guest if the Hard-Coded Test Double is not close at hand.

类 MidnightTimeProvider 实现 TimeProvider {

     public Calendar getTime() {

          Calendar myTime = new GregorianCalendar();

          myTime.set(Calendar.HOUR_OF_DAY, 0);

          myTime.set(Calendar.MINUTE, 0);

          返回 myTime;

    }

}

class  MidnightTimeProvider  implements  TimeProvider  {

     public  Calendar  getTime()  {

          Calendar  myTime  =  new  GregorianCalendar();

          myTime.set(Calendar.HOUR_OF_DAY,  0);

          myTime.set(Calendar.MINUTE,  0);

          return  myTime;

    }

}

 

根据编程语言的不同,此测试替身类可以在许多不同的地方定义,包括在测试用例类(内部类)的主体内,以及作为单独的独立类,与测试位于同一文件中或在其自己的文件中。当然,测试替身类距离测试方法越远,它就越像一个神秘嘉宾

Depending on the programming language, this Test Double Class can be defined in a number of different places, including within the body of the Testcase Class (an inner class) and as a separate free-standing class either in the same file as the test or in its own file. Of course, the farther away the Test Double Class resides from the Test Method, the more of a Mystery Guest it becomes.

例如:自分流/环回

Example: Self Shunt/Loopback

这是使用自我分流来控制时间的测试:

Here's a test that uses a Self Shunt to allow control over the time:

public class SelfShuntExample extends TestCase

implements TimeProvider {

      public void testDisplayCurrentTime_AtMidnight() throws Exception {

          // 固定设置

          TimeDisplay sut = new TimeDisplay();

          // 模拟设置

          sut.setTimeProvider(this); // 自我分流安装

          // 练习 SUT

          String result = sut.getCurrentTimeAsHtmlFragment();

          // 验证直接输出

          String expectedTimeString =

                "<span class=\"tinyBoldText\">Midnight</span>";

          assertEquals("Midnight", expectedTimeString, result);

      }



      public Calendar getTime() {

          Calendar myTime = new GregorianCalendar();

          myTime.set(Calendar.MINUTE, 0);

          myTime.set(Calendar.HOUR_OF_DAY, 0);

          return myTime;

      }

}

public  class  SelfShuntExample  extends  TestCase

implements  TimeProvider  {

      public  void  testDisplayCurrentTime_AtMidnight()  throws  Exception  {

          //  fixture  setup

          TimeDisplay  sut  =  new  TimeDisplay();

          //  mock  setup

          sut.setTimeProvider(this);  //  self  shunt  installation

          //  exercise  SUT

          String  result  =  sut.getCurrentTimeAsHtmlFragment();

          //  verify  direct  output

          String  expectedTimeString  =

                "<span  class=\"tinyBoldText\">Midnight</span>";

          assertEquals("Midnight",  expectedTimeString,  result);

      }



      public  Calendar  getTime()  {

          Calendar  myTime  =  new  GregorianCalendar();

          myTime.set(Calendar.MINUTE,  0);

          myTime.set(Calendar.HOUR_OF_DAY,  0);

          return  myTime;

      }

}

 

请注意,安装硬编码测试替身的测试方法和SUT 调用的方法的实现都是同一个类的成员。我们使用Setter 注入模式(参见依赖注入)来安装硬编码测试替身。由于此示例是用静态类型语言编写的,因此我们必须将子句添加到测试用例类声明中,以便语句可以编译。在动态类型语言中,此步骤是不必要的。getTimeimplements  TimeProvidersut.setTimeProvider(this)

Note how both the Test Method that installs the Hard-Coded Test Double and the implementation of the getTime method called by the SUT are members of the same class. We used the Setter Injection pattern (see Dependency Injection) to install the Hard-Coded Test Double. Because this example is written in a statically typed language, we had to add the clause implements  TimeProvider to the Testcase Class declaration so that the sut.setTimeProvider(this) statement will compile. In a dynamically typed language, this step is unnecessary.

示例:子类化内部测试替身

Example: Subclassed Inner Test Double

下面是一个 JUnit 测试,它使用Java 的“匿名内部类”语法实现了子类内部测试替身:

Here's a JUnit test that uses a Subclassed Inner Test Double using Java's "Anonymous Inner Class" syntax:

public void testDisplayCurrentTime_AtMidnight_AIM() throws Exception {

    // Fixture 设置

    // 定义并实例化测试桩

    TimeProvider testStub = new TimeProvider() {

    // 匿名内部桩

         public Calendar getTime() {

            Calendar myTime = new GregorianCalendar();

            myTime.set(Calendar.MINUTE, 0);

            myTime.set(Calendar.HOUR_OF_DAY, 0);

            return myTime;

      }

    };

    // 实例化 SUT

    TimeDisplay sut = new TimeDisplay();

    // 将测试桩注入 SUT

    sut.setTimeProvider(testStub);

    // 练习 SUT

    String result = sut.getCurrentTimeAsHtmlFragment();

    // 验证直接输出

    String expectedTimeString =

                "<span class=\"tinyBoldText\">Midnight</span>";

    assertEquals("Midnight", expectedTimeString, result);

}

public  void  testDisplayCurrentTime_AtMidnight_AIM()  throws  Exception  {

    //  Fixture  setup

    //        Define  and  instantiate  Test  Stub

    TimeProvider  testStub  =  new  TimeProvider()  {

    //  Anonymous  inner  stub

         public  Calendar  getTime()  {

            Calendar  myTime  =  new  GregorianCalendar();

            myTime.set(Calendar.MINUTE,  0);

            myTime.set(Calendar.HOUR_OF_DAY,  0);

            return  myTime;

      }

    };

    //      Instantiate  SUT

    TimeDisplay  sut  =  new  TimeDisplay();

    //      Inject  Test  Stub  into  SUT

    sut.setTimeProvider(testStub);

    //  Exercise  SUT

    String  result  =  sut.getCurrentTimeAsHtmlFragment();

    //  Verify  direct  output

    String  expectedTimeString  =

                "<span  class=\"tinyBoldText\">Midnight</span>";

    assertEquals("Midnight",  expectedTimeString,  result);

}

 

TimeProvider这里我们在对 的调用中使用了实际依赖类的名称( )new来定义硬编码测试替身getTime。通过在类名后面的花括号内包含方法的定义,我们实际上是在测试方法中创建了一个匿名的子类测试替身

Here we used the name of the real depended-on class (TimeProvider) in the call to new for the definition of the Hard-Coded Test Double. By including a definition of the method getTime within curly braces after the classname, we are actually creating an anonymous Subclassed Test Double inside the Test Method.

示例:从伪类派生出的内部测试替身

Example: Inner Test Double Subclassed from Pseudo-Class

假设我们已经用另一个方法实现替换了该方法的一个实现,而为了向后兼容,我们需要保留该方法,但我们希望编写测试以确保不再调用旧方法。如果我们已经有以下伪对象定义,这很容易做到:

Suppose we have replaced one implementation of a method with another implementation that we need to leave around for backward-compatibility purposes, but we want to write tests to ensure that the old method is no longer called. This is easy to do if we already have the following Pseudo-Object definition:

/**

  * 手工编码测试桩和模拟对象的基类

  */

public class PseudoTimeProvider implements ComplexTimeProvider {



      public Calendar getTime() throws TimeProviderEx {

            throw new PseudoClassException();

      }



      public Calendar getTimeDifference(Calendar baseTime,

                                                              Calendar otherTime)

                    throws TimeProviderEx {

            throw new PseudoClassException();

      }



      public Calendar getTime( String timeZone ) throws TimeProviderEx {

            throw new PseudoClassException();

      }

}

/**

  *  Base  class  for  hand-coded  Test  Stubs  and  Mock  Objects

  */

public  class  PseudoTimeProvider  implements  ComplexTimeProvider  {



      public  Calendar  getTime()  throws  TimeProviderEx  {

            throw  new  PseudoClassException();

      }



      public  Calendar  getTimeDifference(Calendar  baseTime,

                                                              Calendar  otherTime)

                    throws  TimeProviderEx  {

            throw  new  PseudoClassException();

      }



      public  Calendar  getTime(  String  timeZone  )  throws  TimeProviderEx  {

            throw  new  PseudoClassException();

      }

}

 

现在,我们可以编写一个测试,通过子类化和覆盖新版本的方法(我们期望被 SUT 调用的方法)来确保不会调用旧版本getTime的方法

We can now write a test that ensures the old version of the getTime method is not called by subclassing and overriding the newer version of the method (the one we expect to be called by the SUT):

public void testDisplayCurrentTime_AtMidnight_PS() throws Exception {

    // Fixture 设置

    // 定义并实例化测试桩

    TimeProvider testStub = new PseudoTimeProvider()

    { // 匿名内部桩

          public Calendar getTime(String timeZone) {

                Calendar myTime = new GregorianCalendar();

                myTime.set(Calendar.MINUTE, 0);

                myTime.set(Calendar.HOUR_OF_DAY, 0);

                return myTime;

          }

    };

    // 实例化 SUT

    TimeDisplay sut = new TimeDisplay();

    // 将测试桩注入 SUT:

    sut.setTimeProvider(testStub);

    // 练习 SUT

    String result = sut.getCurrentTimeAsHtmlFragment();

    // 验证直接输出

    String expectedTimeString =

                "<span class=\"tinyBoldText\">Midnight</span>";

    assertEquals("Midnight", expectedTimeString, result);

}

public  void  testDisplayCurrentTime_AtMidnight_PS()  throws  Exception  {

    //  Fixture  setup

    //        Define  and  instantiate  Test  Stub

    TimeProvider  testStub  =  new  PseudoTimeProvider()

    {  //  Anonymous  inner  stub

          public  Calendar  getTime(String  timeZone)  {

                Calendar  myTime  =  new  GregorianCalendar();

                myTime.set(Calendar.MINUTE,  0);

                myTime.set(Calendar.HOUR_OF_DAY,  0);

                return  myTime;

          }

    };

    //      Instantiate  SUT

    TimeDisplay  sut  =  new  TimeDisplay();

    //      Inject  Test  Stub  into  SUT:

    sut.setTimeProvider(testStub);

    //  Exercise  SUT

    String  result  =  sut.getCurrentTimeAsHtmlFragment();

    //  Verify  direct  output

    String  expectedTimeString  =

                "<span  class=\"tinyBoldText\">Midnight</span>";

    assertEquals("Midnight",  expectedTimeString,  result);

}

 

如果调用任何其他方法,则将调用基类方法并引发异常。因此,如果我们运行此测试并且调用了我们未覆盖的方法之一,我们将看到以下输出作为此测试错误的 JUnit 堆栈跟踪的第一行:

If any of the other methods are called, the base class methods are invoked and throw an exception. Therefore, if we run this test and one of the methods we didn't override is called, we will see the following output as the first line of the JUnit stack trace for this test error:

com..PseudoClassEx:意外调用不受支持的方法。

在 com..PseudoTimeProvider.getTime(PseudoTimeProvider.java:22)

在 com..TimeDisplay.getCurrentTimeAsHtmlFragment(TimeDisplay.java:64)

在 com..TimeDisplayTestSolution.testDisplayCurrentTime_AtMidnight_PS

     (

          TimeDisplayTestSolution.java:247)

com..PseudoClassEx:  Unexpected  call  to  unsupported  method.

at  com..PseudoTimeProvider.getTime(PseudoTimeProvider.java:22)

at  com..TimeDisplay.getCurrentTimeAsHtmlFragment(TimeDisplay.java:64)

at  com..TimeDisplayTestSolution.

     testDisplayCurrentTime_AtMidnight_PS(

          TimeDisplayTestSolution.java:247)

 

(图案)名称包含什么?

好名字的重要性

名字很重要,因为它们是我们交流的关键部分。名字是我们为概念贴上的标签。好的名字有助于我们传达这些概念。当我们与已经知道名字的人交流时,这一点是正确的,但当我们与不知道名字的人交流时,这一点尤其正确。请考虑以下示例。

在我编写模式的早期,我参加了第一届程序模式语言 (PLoP) 会议 ( http://www.hillside.net/conferences/plop )。在会议上,著名作家 Jim Coplien(他的朋友称他为“Cope”)正在研讨一种组织模式语言。其中一个模式被称为“Buffalo Mountain”;另一个模式被称为“Architect Also Implements”。就模式名称而言,这两个模式名称处于光谱的两端。

即使没有读过实际的模式,也可以从模式名称中看出“架构师也实施”的要点。该名称既是模式的占位符,又具有其本身的意义。

相比之下,“Buffalo Mountain”这个名字并不容易传达其深层含义。直到今天,我仍然记得这个名字背后的故事——但我记不起这个图案的实际重点。这个名字基于一张绘制与图案相关的一些数据的图表。一位早期的评论者认为它类似于附近一座名为 Buffalo Mountain 的山的轮廓。因此,虽然这个图案名称令人难忘,但并不十分引人注目。

更贴近现实的是,Self Shunt(请参见第568页的“硬编码测试替身”)是一个不太令人回味的名称示例,因为“shunt”一词除了在少数专业领域外并未广泛使用。Michael Feathers 在其模式描述中很好地解释了该名称的背景。但是,除非您读过该描述,否则该名称“只是一个名称”。更令人回味的名称可能是“Testcase Class as Test Double”或“Loopback”,但即使是后者也存在歧义,因为不清楚回环的内容是什么。因此,Self Shunt这个名称保留了下来,因为它很常用。

其他命名注意事项

人们可能会问,为什么我有时会为某些图案提出替代名称。前面的故事强调了其中一个原因。另一个原因是,在更大的图案集合(例如本书)中,存在一个“名称系统”非常重要。

让我用一个例子来说明第二个原因。许多人主张使用setUp方法来创建测试装置。这种方法将装置设置逻辑从每个单独的测试方法第 348页)中移出,并移至可以重复使用的单个位置。许多人可能会将此模式称为“共享设置方法”。但在这种模式语言中,我选择将其称为隐式设置第 424页)。为什么?

归根结底还是语言中其他模式的名称。一方面,“共享设置方法”很容易与现有模式共享装置第 317页)混淆。(前一种模式处理共享代码,而后一种模式则侧重于共享装置中的运行时对象。)另一方面,隐式设置的两个主要替代方案称为内联设置(第408页)和委托设置第 411页)。您是否同意“内联设置、委托设置、隐式设置”比“内联设置、委托设置、共享设置方法”形成更好的“名称系统”?当我们在选择名称系统时考虑所有主要替代模式时,模式名称之间的联系更加明显。

为什么要标准化测试模式?

本文的最后一部分重点介绍了我认为标准化测试自动化模式名称的重要性,尤其是与测试桩第 529页)和模拟对象(第544页)相关的名称。这里的关键问题与沟通的简洁性有关。

当有人告诉你“放一个模拟”时(双关语!),这个人给你什么建议?根据这个人所说的“模拟”的含义,他或她可能建议你使用测试桩控制 SUT 的间接输入,或者用假数据库(参见第551页的假对象)替换数据库,这将减少测试交互并将测试速度提高 50 倍。(是的,50 倍!请参阅第319页的侧栏“不使用共享装置进行更快的测试” 。)或者,这个人可能建议你通过安装预先配置了预期行为(参见第 468页的行为验证的 Eager Mock 对象(参见Mock 对象)来验证 SUT 是否调用了正确的方法。如果每个人都用“模拟”来表示Mock 对象— — 不多也不少 — — 那么这个建议就很明确了。当我写这篇文章时,这个建议非常模糊,因为我们已经开始将任何测试替身第 522页)称为“模拟对象”(尽管模拟对象[ET]的原始论文的作者对此表示反对)。

进一步阅读

如果您想了解“Buffalo Mountain”的真正含义,请访问http://www1.bell-labs.com/user/cope/Patterns/Process/section29.html

您可以在http://www1.bell-labs.com/user/cope/Patterns/Process/section16.html找到“建筑师还实现” 。

有趣的是,Alistair Cockburn 在他的网站 ( http://alistair.cockburn.us ) 上的一篇文章中也对形态名称进行了类似的比较,并在比较中选择了完全相同的两个形态名称。巧合还是规律?



What's in a (Pattern) Name?

The Importance of Good Names

Names are important because they are a key part of how we communicate. Names are labels we attach to concepts. Good names help us communicate those concepts. This is true when we are communicating with people who already know the names, but especially when we are communicating with people who don't. Consider the following example.

Early in my pattern-writing days, I attended the very first Pattern Languages of Programs (PLoP) conference (http://www.hillside.net/conferences/plop). At the conference, the well-known author Jim Coplien ("Cope," to his friends) had a pattern language of organizational patterns being workshopped. One of the patterns was called "Buffalo Mountain"; another was called "Architect Also Implements." These two pattern names are at opposite ends of the spectrum as far as pattern names are concerned.

The gist of "Architect Also Implements" can be gleaned from the pattern name even if a person has not read the actual pattern. The name is both a placeholder for the pattern and meaningful in its own right.

The name "Buffalo Mountain," by contrast, does not readily communicate its underlying meaning. To this day I can still remember the story behind the name—but I cannot remember the actual focus of the pattern. The name was based on a graph that plotted some data related to the pattern. An early reviewer thought it resembled the profile of a nearby mountain called Buffalo Mountain. Thus, while the pattern name is memorable, it is not very evocative.

Closer to home, Self Shunt (see Hard-Coded Test Double on page 568) is an example of a name that is less than evocative because the term "shunt" is not widely used except in a few specialized fields. Michael Feathers does a good job explaining the background of the name in his description of the pattern. Unless you've read that description, however, the name is "just a name." A more evocative name might be something like "Testcase Class as Test Double" or "Loopback" but even the latter suffers from ambiguity because it isn't clear what is being looped back. So the name Self Shunt survives because it is in common use.

Other Naming Considerations

People might ask why I sometimes propose alternative names for some patterns. The preceding story highlights one of the reasons. Another reason is that in a larger collection of patterns (such as this book), it is important that there exists a "system of names."

Let me illustrate this second reason with an example. Many people advocate the use of a setUp method to create the test fixture. This approach moves the fixture setup logic out of each individual Test Method (page 348) and into a single place where it can be reused. Many people might refer to this pattern as "Shared Setup Method." But in this pattern language, I've chosen to call it Implicit Setup (page 424). Why?

It comes down to the names of other patterns in the language. On the one hand, "Shared Setup Method" could easily be confused with the existing pattern Shared Fixture (page 317). (The former pattern deals with sharing code, whereas the latter pattern focuses on sharing the runtime objects in the fixture.) On the other hand, the two major alternatives to Implicit Setup are called In-line Setup (page 408) and Delegated Setup (page 411). Wouldn't you agree that "In-line Setup, Delegated Setup, Implicit Setup" forms a better "system of names" than "In-line Setup, Delegated Setup, Shared Setup Method"? The connection between the pattern names is much more obvious when we consider all the major alternative patterns when choosing the system of names.

Why Standardize Testing Patterns?

The last part of this soapbox highlights why I think it is important for us to standardize the names of the test automation patterns, especially those related to Test Stubs (page 529) and Mock Objects (page 544). The key issue here relates to succinctness of communication.

When someone tells you, "Put a mock in it" (pun intended!), what advice is that person giving you? Depending on what the person means by a "mock," he or she could be suggesting that you control the indirect inputs of your SUT using a Test Stub or that you replace your database with a Fake Database (see Fake Object on page 551) that will reduce test interactions and speed up your tests by a factor of 50. (Yes, 50! See the sidebar "Faster Tests Without Shared Fixtures" on page 319.) Or perhaps the person is suggesting that you verify that your SUT calls the correct methods by installing an Eager Mock Object (see Mock Object) preconfigured with the Expected Behavior (see Behavior Verification on page 468). If everyone used "mock" to mean a Mock Object—no more or less—then the advice would be pretty clear. As I write this, the advice is very murky because we have taken to calling just about any Test Double (page 522) a "mock object" (despite the objections of the authors of the original paper on Mock Objects [ET]).

Further Reading

If you want to find out what "Buffalo Mountain" is really about, go to http://www1.bell-labs.com/user/cope/Patterns/Process/section29.html.

You can find "Architect Also Implements" at http://www1.bell-labs.com/user/cope/Patterns/Process/section16.html.

Interestingly, Alistair Cockburn wrote a similar comparison of pattern names in an article on his Web site (http://alistair.cockburn.us) and chose exactly the same two pattern names in his comparison. Coincidence or pattern?


 

除了测试失败之外,此方案还使您可以非常轻松地查看到底调用了哪个方法好处是,它适用于调用所有意外方法,无需额外努力。

In addition to failing the test, this scheme makes it very easy to see exactly which method was called. The bonus is that it works for calls to all unexpected methods with no additional effort.

进一步阅读

许多关于测试驱动开发的“如何做”书籍都提供了Self Shunt 的示例,包括[TDD-APG][TDD-BE][UTwJ][PUT][JuPG]。原始文章由 Michael Feathers 撰写,可在http://www.objectmentor.com/resources/articles/SelfShunPtrn.pdf上找到

Many of the "how to" books on test-driven development provide examples of Self Shunt, including [TDD-APG], [TDD-BE], [UTwJ], [PUT], and [JuPG]. The original write-up was by Michael Feathers and is accessible at http://www.objectmentor.com/resources/articles/SelfShunPtrn.pdf

原始的“分流”模式写在http://http://c2.com/cgi/wiki? ShuntPattern,以及包括“Loopback”在内的备选名称列表。请参阅第 576页的侧栏“ (模式)名称包含什么? ” ,了解如何选择有意义且令人回味的模式名称的讨论。

The original "Shunt" pattern is written up at http://http://c2.com/cgi/wiki? ShuntPattern, along with a list of alternative names including "Loopback." See the sidebar "What's in a (Pattern) Name?" on page 576 for a discussion of how to select meaningful and evocative pattern names.

伪对象模式在论文“伪类:用于单元测试的非常简单和轻量级的模拟对象类”中进行了描述,网址为http://www.devx.com/Java/Article/22599/1954?pf=true

The Pseudo-Object pattern is described in the paper "Pseudo-Classes: Very Simple and Lightweight Mock Object-like Classes for Unit-Testing" available at http://www.devx.com/Java/Article/22599/1954?pf=true.

测试专用子类

Test-Specific Subclass

当我们需要访问 SUT 的私有状态时,如何使代码可测试?

How can we make code testable when we need to access private state of the SUT?

我们添加了将测试所需的状态或行为公开给 SUT 子类的方法。

We add methods that expose the state or behavior needed by the test to a subclass of the SUT.

也称为

Also known as

测试专用扩展

Test-Specific Extension

图像

如果 SUT 不是专门设计为可测试的,我们可能会发现测试无法访问在测试的某个时刻必须初始化或验证的状态。

If the SUT was not designed specifically to be testable, we may find that the test cannot gain access to a state that it must initialize or verify at some point in the test.

测试特定子类是一种简单但非常强大的方法,可以开放 SUT 以用于测试目的,而无需修改 SUT 本身的代码。

A Test-Specific Subclass is a simple yet very powerful way to open up the SUT for testing purposes without modifying the code of the SUT itself.

工作原理

How It Works

我们定义 SUT 的子类,并添加一些方法,这些方法通过实现控制点和观察点来修改 SUT 的行为,使其易于测试。这项工作通常涉及使用 setter 和 getter 公开实例变量,或者添加一种方法将 SUT 置于特定状态,而无需经历其整个生命周期。

We define a subclass of the SUT and add methods that modify the behavior of the SUT just enough to make it testable by implementing control points and observation points. This effort typically involves exposing instance variables using setters and getters or perhaps adding a method to put the SUT into a specific state without moving through its entire life cycle.

因为测试特定子类将与使用它的测试一起打包,所以测试特定子类的使用不会改变应用程序其余部分对 SUT 的看法。

Because the Test-Specific Subclass would be packaged together with the tests that use it, the use of a Test-Specific Subclass does not change how the SUT is seen by the rest of the application.

何时使用它

When to Use It

每当我们需要修改 SUT 以提高其可测试性时,我们都应该使用测试特定子类,但直接这样做会导致“生产中的测试逻辑”第 217页)。尽管我们可以将测试特定子类用于多种目的,但所有这些场景都有一个共同的目标:通过让我们更容易地了解 SUT 的内部情况,它们可以提高可测试性。然而,测试特定子类可能是一把双刃剑。通过打破封装,它允许我们将测试与实现更紧密地联系在一起,这反过来会导致“脆弱的测试”第 239页)。

We should use a Test-Specific Subclass whenever we need to modify the SUT to improve its testability but doing so directly would result in Test Logic in Production (page 217). Although we can use a Test-Specific Subclass for a number of purposes, all of those scenarios share a common goal: They improve testability by letting us get at the insides of the SUT more easily. A Test-Specific Subclass can be a double-edged sword, however. By breaking encapsulation, it allows us to tie our tests even more closely to the implementation, which can in turn result in Fragile Tests (page 239).

变体:状态暴露子类

如果我们正在进行状态验证(第 462页),我们可以将 SUT (或其中的某个组件) 子类化,以便我们可以看到 SUT 的内部状态,以便在断言方法(第 362页) 中使用。通常,这项工作涉及为私有实例变量添加访问器方法。我们还可以允许测试设置状态,以避免由模糊设置(参见模糊测试) 逻辑导致的模糊测试 (第 186页)。

If we are doing State Verification (page 462), we can subclass the SUT (or some component of it) so that we can see the internal state of the SUT for use in Assertion Methods (page 362). Usually, this effort involves adding accessor methods for private instance variables. We may also allow the test to set the state as a way to avoid Obscure Tests (page 186) caused by Obscure Setup (see Obscure Test) logic.

变体:行为暴露子类

如果我们想单独测试复杂算法的各个步骤,我们可以将 SUT 子类化以公开实现自调用[WWW] 的私有方法。由于大多数语言不允许放宽方法的可见性,因此我们经常不得不在测试特定子类中使用不同的名称并调用超类的方法。

If we want to test the individual steps of a complex algorithm individually, we can subclass the SUT to expose the private methods that implement the Self-Calls [WWW]. Because most languages do not allow for relaxing the visibility of a method, we often have to use a different name in the Test-Specific Subclass and make a call to the superclass's method.

变体:行为修改子类

如果 SUT 包含一些我们不希望在测试时发生的行为,我们可以用空方法体覆盖实现该行为的任何方法。当 SUT 使用自调用(或模板方法[GOF])将算法的步骤委托给其自身或子类的方法时,此技术效果最佳。

If the SUT contains some behavior that we do not want to occur when testing, we can override whatever method implements the behavior with an empty method body. This technique works best when the SUT uses Self-Calls (or a Template Method [GOF]) to delegate the steps of an algorithm to methods on itself or subclasses.

变体:测试替身子类

为了确保测试替身(第 522页) 与我们希望替换的 DOC 类型兼容,我们可以将测试替身设为该组件的子类。这可能是我们构建测试替身的唯一方法,当变量使用具体类进行静态类型化时,编译器会接受该测试替身。5(对于动态类型语言,如 Ruby、Python、Perl 和 JavaScript,我们不必采取这一步骤。)然后,我们重写任何我们想要更改其行为的方法,并添加任何我们需要的方法,以将测试替身转换为可配置测试替身(第 558页)(如果我们愿意的话)。

To ensure that a Test Double (page 522) is type-compatible with a DOC we wish to replace, we can make the Test Double a subclass of that component. This may be the only way we can build a Test Double that the compiler will accept when variables are statically typed using concrete classes.5 (We should not have to take this step with dynamically typed languages such as Ruby, Python, Perl, and JavaScript.) We then override any methods whose behavior we want to change and add any methods we require to transform the Test Double into a Configurable Test Double (page 558) if we so desire.

与行为修改子类不同,测试替身子类不只是“调整” SUT 的行为(或其一部分),而是用预定行为完全取代它。

Unlike the Behavior-Modifying Subclass, the Test Double Subclass does not just "tweak" the behavior of the SUT (or a part thereof) but replaces it entirely with canned behavior.

也称为

Also known as

子类测试替身

Subclassed Test Double

变体:替代单例

也称为

Also known as

子类单例,可替代单例

Subclassed Singleton, Substitutable Singleton

替代单例是测试替身子类的一个特例。当我们想用测试替身替换 DOC并且 SUT 不支持依赖注入(第 678页) 或依赖查找(第 686页) 时,我们会使用它。

The Substituted Singleton is a special case of Test Double Subclass. We use it when we want to replace a DOC with a Test Double and the SUT does not support Dependency Injection (page 678) or Dependency Lookup (page 686).

实施说明

Implementation Notes

使用特定测试子类会带来一些挑战:

The use of a Test-Specific Subclass brings some challenges:

  • 功能粒度:确保我们想要覆盖或公开的任何行为都在其自己的单一用途方法中。通过大量使用小方法和自调用来实现。
  • Feature granularity: ensuring that any behavior we want to override or expose is in its own single-purpose method. It is enabled through copious use of small methods and Self-Calls.
  • 特性可见性:确保子类可以访问 SUT 类的属性和行为。这主要是静态类型语言(如 Java、C# 和 C++)中的问题;动态类型语言通常不强制可见性。
  • Feature visibility: ensuring that subclasses can access attributes and behavior of the SUT class. It is primarily an issue in statically typed languages such as Java, C#, and C++; dynamically typed languages typically do not enforce visibility.

测试替身一样,我们必须小心确保不会替换我们实际尝试测试的任何行为。

As with Test Doubles, we must be careful to ensure that we do not replace any of the behavior we are actually trying to test.

在支持类扩展而不需要子类化的语言(例如 Smalltalk、Ruby、JavaScript 和其他动态语言)中,可以将测试特定子类实现为测试包中的类扩展。但是,我们需要注意扩展是否会投入生产;这样做会引入生产中的测试逻辑

In languages that support class extensions without the need for subclassing (e.g., Smalltalk, Ruby, JavaScript, and other dynamic languages), a Test-Specific Subclass can be implemented as a class extension in the test package. We need to be aware, however, whether the extensions will make it into production; doing so would introduce Test Logic in Production.

特征可见性

在强制变量和方法的范围(可见性)的语言中,我们可能需要更改变量的可见性以允许子类访问它们。虽然这样的更改会影响实际的 SUT 代码,但通常认为它比将可见性更改为public(从而允许应用程序中的任何代码访问变量)或将特定于测试的方法直接添加到 SUT 的侵入性或误导性要小得多。

In languages that enforce scope (visibility) of variables and methods, we may need to change the visibility of the variables to allow subclasses to access them. While such a change affects the actual SUT code, it would typically be considered much less intrusive or misleading than changing the visibility to public (thereby allowing any code in the application to access the variables) or adding the test-specific methods directly to the SUT.

例如,在 Java 中,我们可以将实例变量的可见性从 更改为privateprotected允许特定测试子类访问它们。同样,我们可以将方法的可见性更改为允许特定测试子类调用它们。

For example, in Java, we might change the visibility of instance variables from private to protected to allow the Test-Specific Subclass to access them. Similarly, we might change the visibility of methods to allow the Test-Specific Subclass to call them.

特征的粒度

长方法很难测试,因为它们通常会引入太多依赖项。相比之下,短方法往往更容易测试,因为它们只做一件事。自调用提供了一种减少方法大小的简单方法。我们将算法的各个部分委托给在同一类上实现的其他方法。这种策略使我们能够独立测试这些方法。我们还可以通过在测试替身子类中重写这些方法请参阅第 579页的测试特定子类)来确认调用方法是否按正确的顺序调用这些方法。

Long methods are difficult to test because they often bring too many dependencies into play. By comparison, short methods tend to be much simpler to test because they do only one thing. Self-Call offers an easy way to reduce the size of methods. We delegate parts of an algorithm to other methods implemented on the same class. This strategy allows us to test these methods independently. We can also confirm that the calling method calls these methods in the right sequence by overriding them in a Test Double Subclass (see Test-Specific Subclass on page 579).

自调用是良好面向对象代码设计的一部分,因为它使方法保持简短并专注于实现 SUT 的单一职责。每当我们进行测试驱动开发并控制 SUT 的设计时,我们都可以使用此模式。当我们遇到长方法时,我们可能会发现需要引入自调用,因为算法的某些部分依赖于我们不想执行的内容(例如数据库调用)。例如,当使用事务脚本 [PEAA]架构构建 SUT 时,这种可能性尤其高。使用大多数现代 IDE 支持的提取方法 [Fowler] 重构可以轻松改造自调用。

Self-Call is a part of good object-oriented code design in that it keeps methods small and focused on implementing a single responsibility of the SUT. We can use this pattern whenever we are doing test-driven development and have control over the design of the SUT. We may find that we need to introduce Self-Call when we encounter long methods where some parts of the algorithm depend on things we do not want to exercise (e.g., database calls). This likelihood is especially high, for example, when the SUT is built using a Transaction Script [PEAA] architecture. Self-Call can be retrofitted easily using the Extract Method [Fowler] refactoring supported by most modern IDEs.

激励人心的例子

Motivating Example

以下示例中的测试是不确定的,因为它取决于时间。我们的 SUT 是一个对象,它将时间格式化为网页的一部分进行显示。它通过要求调用的 SingletonTimeProvider从容器中获取的日历对象中检索时间来获取时间。

The test in the following example is nondeterministic because it depends on the time. Our SUT is an object that formats the time for display as part of a Web page. It gets the time by asking a Singleton called TimeProvider to retrieve the time from a calendar object that it gets from the container.

public void testDisplayCurrentTime_AtMidnight() throws Exception {

      // 设置 SUT

      TimeDisplay theTimeDisplay = new TimeDisplay();

      // 练习 SUT

      String actualTimeString =

                theTimeDisplay.getCurrentTimeAsHtmlFragment();

      // 验证结果

      String expectedTimeString =

                "<span class=\"tinyBoldText\">Midnight</span>";

      assertEquals( "Midnight",

                            expectedTimeString,

                            actualTimeString);

}



public void testDisplayCurrentTime_AtOneMinuteAfterMidnight()

              throws Exception {

      // 设置 SUT

      TimeDisplay actualTimeDisplay = new TimeDisplay();

      // 练习 SUT

      String actualTimeString =

              actualTimeDisplay.getCurrentTimeAsHtmlFragment();

      // 验证结果

      String expectedTimeString =

                  "<span class=\"tinyBoldText\">12:01 AM</span>";

      断言Equals(“12:01 AM”,

                            预期时间字符串,

                            实际时间字符串);

}

public  void  testDisplayCurrentTime_AtMidnight()  throws  Exception  {

      //  Set  up  SUT

      TimeDisplay  theTimeDisplay  =  new  TimeDisplay();

      //  Exercise  SUT

      String  actualTimeString  =

                theTimeDisplay.getCurrentTimeAsHtmlFragment();

      //  Verify  outcome

      String  expectedTimeString  =

                "<span  class=\"tinyBoldText\">Midnight</span>";

      assertEquals(  "Midnight",

                            expectedTimeString,

                            actualTimeString);

}



public  void  testDisplayCurrentTime_AtOneMinuteAfterMidnight()

              throws  Exception  {

      //  Set  up  SUT

      TimeDisplay  actualTimeDisplay  =  new  TimeDisplay();

      //  Exercise  SUT

      String  actualTimeString  =

              actualTimeDisplay.getCurrentTimeAsHtmlFragment();

      //  Verify  outcome

      String  expectedTimeString  =

                  "<span  class=\"tinyBoldText\">12:01  AM</span>";

      assertEquals(  "12:01  AM",

                            expectedTimeString,

                            actualTimeString);

}

 

这些测试很少通过,而且它们从来不会在同一次测试运行中通过!SUT 中的代码如下所示:

These tests rarely pass, and they never pass in the same test run! The code within the SUT looks like this:

公共字符串 getCurrentTimeAsHtmlFragment() {

      日历 timeProvider;

      尝试 {

            timeProvider = getTime();

      } 捕获 (异常 e) {

            返回 e.getMessage();

      }

            // 等等

}



受保护的日历 getTime() {

      返回 TimeProvider.getInstance().getTime();

}

public  String  getCurrentTimeAsHtmlFragment()  {

      Calendar  timeProvider;

      try  {

            timeProvider  =  getTime();

      }  catch  (Exception  e)  {

            return  e.getMessage();

      }

            //  etc.

}



protected  Calendar  getTime()  {

      return  TimeProvider.getInstance().getTime();

}

 

Singleton 的代码如下:

The code for the Singleton follows:

公共类 TimeProvider {

      protected static TimeProvider soleInstance = null;



      protected TimeProvider() {};



      公共静态 TimeProvider getInstance() {



            if (soleInstance==null) soleInstance = new TimeProvider();

            返回 soleInstance;

      }

      公共 Calendar getTime() {

            返回 Calendar.getInstance();

      }

}

public  class  TimeProvider  {

      protected  static  TimeProvider  soleInstance  =  null;



      protected  TimeProvider()  {};



      public  static  TimeProvider  getInstance()  {



            if  (soleInstance==null)  soleInstance  =  new  TimeProvider();

            return  soleInstance;

      }

      public  Calendar  getTime()  {

            return  Calendar.getInstance();

      }

}

 

重构说明

Refactoring Notes

引入测试特定子类所采用的重构的确切性质取决于我们使用的原因。当我们使用测试特定子类来公开 SUT 的“私有部分”或覆盖其行为中不需要的部分时,我们只需将测试特定子类定义为 SUT 的子类,并创建测试特定子类的一个实例,以便在四阶段测试(第 358页)的设置装置阶段进行练习。

The precise nature of the refactoring employed to introduce a Test-Specific Subclass depends on why we are using one. When we are using a Test-Specific Subclass to expose "private parts" of the SUT or override undesirable parts of its behavior, we merely define the Test-Specific Subclass as a subclass of the SUT and create an instance of the Test-Specific Subclass to exercise in the setup fixture phase of our Four-Phase Test (page 358).

然而,当我们使用测试特定子类来替换 SUT 的 DOC 时,我们需要使用用测试替身替换依赖项(第 522页)重构来告诉 SUT 使用我们的测试特定子类而不是真正的 DOC。

When we are using the Test-Specific Subclass to replace a DOC of the SUT, however, we need to use a Replace Dependency with Test Double (page 522) refactoring to tell the SUT to use our Test-Specific Subclass instead of the real DOC.

无论哪种情况,我们都会根据测试的需要,使用特定于语言的功能(例如,子类化或混合)覆盖现有方法或向特定于测试的子类添加新方法。

In either case, we either override existing methods or add new methods to the Test-Specific Subclass using our language-specific capabilities (e.g., subclassing or mixins) as required by our tests.

示例:行为修改子类(测试桩)

Example: Behavior-Modifying Subclass (Test Stub)

因为 SUT 使用方法的自调用getTime来询问TimeProvider时间,所以我们有机会使用子类测试替身来控制时间。6基于这个想法,我们可以尝试编写如下测试(我在这里只展示了一个测试):

Because the SUT uses a Self-Call to the getTime method to ask the TimeProvider for the time, we have an opportunity to use a Subclassed Test Double to control the time.6 Based on this idea we can take a stab at writing our tests as follows (I have shown only one test here):

public void testDisplayCurrentTime_AtMidnight() {

      // 固定装置设置

      TimeDisplayTestStubSubclass tss = new TimeDisplayTestStubSubclass();

      TimeDisplay sut = tss;

      // 测试替身配置

      tss.setHours(0);

      tss.setMinutes(0);

      // 练习 SUT

      String result = sut.getCurrentTimeAsHtmlFragment();

      // 验证结果

      String expectedTimeString =

                      "<span class=\"tinyBoldText\">Midnight</span>";

      assertEquals( expectedTimeString, result );

}

public  void  testDisplayCurrentTime_AtMidnight()  {

      //  Fixture  setup

      TimeDisplayTestStubSubclass  tss  =  new  TimeDisplayTestStubSubclass();

      TimeDisplay  sut  =  tss;

      //      Test  Double  configuration

      tss.setHours(0);

      tss.setMinutes(0);

      //  Exercise  SUT

      String  result  =  sut.getCurrentTimeAsHtmlFragment();

      //  Verify  outcome

      String  expectedTimeString  =

                      "<span  class=\"tinyBoldText\">Midnight</span>";

      assertEquals(  expectedTimeString,  result  );

}

 

请注意,我们已经将测试特定子类用作接收 SUT 实例的变量;这种方法确保在测试特定子类上定义的配置接口(请参阅可配置测试替身)的方法对测试可见。7出于文档目的,我们将测试特定子类分配给变量;这是一种安全的转换,因为测试特定子类是 SUT 类的子类。这种技术还可以帮助我们避免因在测试桩(第529)内对 SUT 的重要间接输入进行硬编码而导致的神秘访客(请参阅模糊测试)问题。sut

Note that we have used the Test-Specific Subclass class for the variable that receives the instance of the SUT; this approach ensures that the methods of the Configuration Interface (see Configurable Test Double) defined on the Test-Specific Subclass are visible to the test.7 For documentation purposes, we have then assigned the Test-Specific Subclass to the variable sut; this is a safe cast because the Test-Specific Subclass class is a subclass of the SUT class. This technique also helps us avoid the Mystery Guest (see Obscure Test) problem caused by hard-coding an important indirect input of our SUT inside the Test Stub (page 529).

现在我们已经了解了如何使用它,实现特定测试子类就很简单了:

Now that we have seen how it will be used, it is a simple matter to implement the Test-Specific Subclass:

public class TimeDisplayTestStubSubclass extends TimeDisplay {



      private int hours;

      private int minutes;



      // 重写方法

      protected Calendar getTime() {

            Calendar myTime = new GregorianCalendar();

            myTime.set(Calendar.HOUR_OF_DAY, this.hours);

            myTime.set(Calendar.MINUTE, this.minutes);

            return myTime;

      }

      /*

        * 配置接口

        */

      public void setHours(int hours) {

            this.hours = hours;

      }



      public void setMinutes(int minutes) {

            this.minutes = minutes;

      }

}

public  class  TimeDisplayTestStubSubclass  extends  TimeDisplay  {



      private  int  hours;

      private  int  minutes;



      //  Overridden  method

      protected  Calendar  getTime()  {

            Calendar  myTime  =  new  GregorianCalendar();

            myTime.set(Calendar.HOUR_OF_DAY,  this.hours);

            myTime.set(Calendar.MINUTE,  this.minutes);

            return  myTime;

      }

      /*

        *  Configuration  Interface

        */

      public  void  setHours(int  hours)  {

            this.hours  =  hours;

      }



      public  void  setMinutes(int  minutes)  {

            this.minutes  =  minutes;

      }

}

 

这里没有什么火箭科学——我们只需要实施测试所使用的方法。

There's no rocket science here—we just had to implement the methods used by the test.

示例:行为修改子类(替代单例)

Example: Behavior-Modifying Subclass (Substituted Singleton)

假设我们的getTime方法被声明为private8staticfinalsealed,等等。9这样的声明会阻止我们在测试特定子类中覆盖方法的行为。我们该怎么做才能解决我们的非确定性测试(请参阅第228页的不稳定测试)?

Suppose our getTime method was declared to be private8 or static, final or sealed, and so on.9 Such a declaration would prevent us from overriding the method's behavior in our Test-Specific Subclass. What could we do to address our Nondeterministic Tests (see Erratic Test on page 228)?

因为设计使用 Singleton [GOF]来提供时间,所以一个简单的解决方案是在测试执行期间用Test Double Subclass替换 Singleton 。只要子类可以访问其soleInstance变量,我们就可以这样做。我们使用 Introduce Local Extension [Fowler] 重构(具体来说,是它的子类变体)来创建Test-Specific Subclass。首先编写测试有助于我们了解要实现的接口。

Because the design uses a Singleton [GOF] to provide the time, a simple solution is to replace the Singleton during test execution with a Test Double Subclass. We can do so as long as it is possible for a subclass to access its soleInstance variable. We use the Introduce Local Extension [Fowler] refactoring (specifically, the subclass variant of it) to create the Test-Specific Subclass. Writing the tests first helps us understand the interface we want to implement.

public void testDisplayCurrentTime_AtMidnight() {

      TimeDisplay sut = new TimeDisplay();

      // 安装测试单例

      TimeProviderTestSingleton timeProvideSingleton =

                TimeProviderTestSingleton.overrideSoleInstance();

      timeProvideSingleton.setTime(0,0);

      // 练习 SUT

      String actualTimeString = sut.getCurrentTimeAsHtmlFragment();

      // 验证结果

      String expectedTimeString =

               "<span class=\"tinyBoldText\">Midnight</span>";

      assertEquals( expectedTimeString, actualTimeString );

}

public  void  testDisplayCurrentTime_AtMidnight()  {

      TimeDisplay  sut  =  new  TimeDisplay();

      //      Install  test  Singleton

      TimeProviderTestSingleton  timeProvideSingleton  =

                TimeProviderTestSingleton.overrideSoleInstance();

      timeProvideSingleton.setTime(0,0);

      //      Exercise  SUT

      String  actualTimeString  =  sut.getCurrentTimeAsHtmlFragment();

      //  Verify  outcome

      String  expectedTimeString  =

               "<span  class=\"tinyBoldText\">Midnight</span>";

      assertEquals(  expectedTimeString,  actualTimeString  );

}

 

现在我们有一个使用替代单例的测试,我们可以通过子类化单例并定义测试将使用的方法来实现它。

Now that we have a test that uses the Substituted Singleton, we can proceed to implement it by subclassing the Singleton and defining the methods the tests will use.

public class TimeProviderTestSingleton extends TimeProvider {

      private Calendar myTime = new GregorianCalendar();

      private TimeProviderTestSingleton() {};



      // 安装接口

      static TimeProviderTestSingleton overrideSoleInstance() {

          // 我们可以先保存真实实例,但我们不会这样做!

          soleInstance = new TimeProviderTestSingleton();

          return (TimeProviderTestSingleton) soleInstance;

      }



      // 测试使用的配置接口

      public void setTime(int hours, int minutes) {

            myTime.set(Calendar.HOUR_OF_DAY, hours);

            myTime.set(Calendar.MINUTE, minutes);

      }



      // 客户端使用的使用接口

      public Calendar getTime() {

            return myTime;

      }

}

public  class  TimeProviderTestSingleton  extends  TimeProvider  {

      private  Calendar  myTime  =  new  GregorianCalendar();

      private  TimeProviderTestSingleton()  {};



      //  Installation  Interface

      static  TimeProviderTestSingleton  overrideSoleInstance()  {

          //  We  could  save  the  real  instance  first,  but  we  won't!

          soleInstance  =  new  TimeProviderTestSingleton();

          return  (TimeProviderTestSingleton)  soleInstance;

      }



      //  Configuration  Interface  used  by  the  test

      public  void  setTime(int  hours,  int  minutes)  {

            myTime.set(Calendar.HOUR_OF_DAY,  hours);

            myTime.set(Calendar.MINUTE,  minutes);

      }



      //  Usage  Interface  used  by  the  client

      public  Calendar  getTime()  {

            return  myTime;

      }

}

 

这里的测试替身是真实组件的子类,并重写了单例客户端调用的实例方法。

Here the Test Double is a subclass of the real component and has overridden the instance method called by the clients of the Singleton.

示例:行为公开子类

Example: Behavior-Exposing Subclass

假设我们想getTime直接测试该方法。由于getTimeprotected我们的测试与TimeDisplay类位于不同的包中,因此我们的测试无法调用此方法。我们可以尝试将我们的测试设为 的子类,TimeDisplay或者将其放入与 相同的包中TimeDisplay。不幸的是,这两种解决方案都有问题,并且可能并不总是可行的。

Suppose we wanted to test the getTime method directly. Because getTime is protected and our test is in a different package from the TimeDisplay class, our test cannot call this method. We could try making our test a subclass of TimeDisplay or we could put it into the same package as TimeDisplay. Unfortunately, both of these solutions come with baggage and may not always be possible.

更通用的解决方案是使用Behavior-Exposing Subclass来暴露行为。我们可以通过定义一个Test-Specific Subclass并添加一个public调用此方法的方法来实现。

A more general solution is to expose the behavior using a Behavior-Exposing Subclass. We can do so by defining a Test-Specific Subclass and adding a public method that calls this method.

公共类 TimeDisplayBehaviorExposingTss 扩展了 TimeDisplay {



      公共日历 callGetTime() {

            返回 super.getTime();

      }

}

public  class  TimeDisplayBehaviorExposingTss  extends  TimeDisplay  {



      public  Calendar  callGetTime()  {

            return  super.getTime();

      }

}

 

我们现在可以使用Behavior-Exposing Subclass编写测试,如下所示:

We can now write the test using the Behavior-Exposing Subclass as follows:

public void testGetTime_default() {

    // 创建 SUT

    TimeDisplayBehaviorExposingTss tsSut =

                   new TimeDisplayBehaviorExposingTss();

    // 练习 SUT

    // 想要做

    // Calendar time = sut.getTime();

    // 必须做

    Calendar time = tsSut.callGetTime();

    // 验证结果

    assertEquals( defaultTime, time );

}

public  void  testGetTime_default()  {

    //  create  SUT

    TimeDisplayBehaviorExposingTss  tsSut  =

                   new  TimeDisplayBehaviorExposingTss();

    //  exercise  SUT

    //    want  to  do

    //        Calendar  time  =  sut.getTime();

    //    have  to  do

    Calendar  time  =  tsSut.callGetTime();

    //  verify  outcome

    assertEquals(  defaultTime,  time  );

}

 

示例:定义测试特定的相等性(行为修改子类)

Example: Defining Test-Specific Equality (Behavior-Modifying Subclass)

这是一个非常简单的测试示例,该测试失败是因为我们传递给的对象assertEquals未实现测试特定的相等性。也就是说,即使我们的测试认为两个对象相等,默认equals方法也会返回。false

Here is an example of a very simple test that fails because the object we pass to assertEquals does not implement test-specific equality. That is, the default equals method returns false even though our test considers the two objects to be equals.

protected void setUp() throws Exception {

      oneOutboundFlight = findOneOutboundFlightDto();

}



public void testGetFlights_OneFlight() throws Exception {

      // 练习系统

      列表 flights = Facade.getFlightsByOriginAirport(

                             oneOutboundFlight.getOriginAirportId());

      // 验证结果

      assertEquals("出发地的航班 - 航班数量: ",

                         1,

                         flights.size());

      FlightDto actualFlightDto = (FlightDto)flights.get(0);

      assertEquals("出发地的航班 DTO",

                         oneOutboundFlight,

                         actualFlightDto);

}

protected  void  setUp()  throws  Exception  {

      oneOutboundFlight  =  findOneOutboundFlightDto();

}



public  void  testGetFlights_OneFlight()  throws  Exception  {

      //  Exercise  System

      List  flights  =  facade.getFlightsByOriginAirport(

                             oneOutboundFlight.getOriginAirportId());

      //  Verify  Outcome

      assertEquals("Flights  at  origin  -  number  of  flights:  ",

                         1,

                         flights.size());

      FlightDto  actualFlightDto  =  (FlightDto)flights.get(0);

      assertEquals("Flight  DTOs  at  origin",

                         oneOutboundFlight,

                         actualFlightDto);

}

 

一种选择是编写自定义断言第 474页)。另一种选择是使用测试特定子类来添加更适合我们测试目的的相等性定义。我们可以稍微更改我们的夹具设置代码,以创建测试特定子类作为我们的预期对象(请参阅状态验证)。

One option is to write a Custom Assertion (page 474). Another option is to use a Test-Specific Subclass to add a more appropriate definition of equality for our test purposes alone. We can change our fixture setup code slightly to create the Test-Specific Subclass as our Expected Object (see State Verification).

私人 FlightDtoTss oneOutboundFlight;



私人 FlightDtoTss findOneOutboundFlightDto() {

      FlightDto realDto = helper.findOneOutboundFlightDto();

      返回新的 FlightDtoTss(realDto) ;

}

private  FlightDtoTss  oneOutboundFlight;



private  FlightDtoTss  findOneOutboundFlightDto()  {

      FlightDto  realDto  =  helper.findOneOutboundFlightDto();

      return  new  FlightDtoTss(realDto)  ;

}

 

最后,我们通过复制和比较那些我们想要用于特定测试相等性的字段来实现特定测试子类。

Finally, we implement the Test-Specific Subclass by copying and comparing only those fields that we want to use for our test-specific equality.

公共类 FlightDtoTss 扩展了 FlightDto {

      公共 FlightDtoTss(FlightDto realDto) {

            this.destAirportId = realDto.getDestinationAirportId();

            this.equipmentType = realDto.getEquipmentType();

            this.flightNumber = realDto.getFlightNumber();

            this.originAirportId = realDto.getOriginAirportId();

      }



      公共布尔值 equals(Object obj) {

            FlightDto otherDto = (FlightDto) obj;

            如果 (otherDto == null) 返回 false;

            如果 (otherDto.getDestAirportId()!= this.destAirportId)

                返回 false;

            如果 (otherDto.getOriginAirportId()!= this.originAirportId)

                返回 false;

            如果 (otherDto.getFlightNumber()!= this.flightNumber)

                返回 false;

            如果 (otherDto.getEquipmentType() != this.equipmentType )

                返回 false;

            返回 true;

      }

}

public  class  FlightDtoTss  extends  FlightDto  {

      public  FlightDtoTss(FlightDto  realDto)  {

            this.destAirportId  =  realDto.getDestinationAirportId();

            this.equipmentType  =  realDto.getEquipmentType();

            this.flightNumber  =  realDto.getFlightNumber();

            this.originAirportId  =  realDto.getOriginAirportId();

      }



      public  boolean  equals(Object  obj)  {

            FlightDto  otherDto  =  (FlightDto)  obj;

            if  (otherDto  ==  null)  return  false;

            if  (otherDto.getDestAirportId()!=  this.destAirportId)

                return  false;

            if  (otherDto.getOriginAirportId()!=  this.originAirportId)

                return  false;

            if  (otherDto.getFlightNumber()!=  this.flightNumber)

                return  false;

            if  (otherDto.getEquipmentType()  !=  this.equipmentType  )

                return  false;

            return  true;

      }

}

 

在本例中,我们将字段从实际 DTO 复制到了Test-Specific Subclass中,但我们也可以很轻松地将Test-Specific Subclass用作实际 DTO 的包装器。我们还可以采用其他方式创建Test-Specific Subclass;唯一的限制就是我们的想象力。

In this case we copied the fields from the real DTO into our Test-Specific Subclass, but we could just as easily have used the Test-Specific Subclass as a wrapper for the real DTO. There are other ways we could have created the Test-Specific Subclass; the only real limit is our imagination.

此示例还假设我们toString在基类上有一个合理的实现,可以打印出被比较字段的值。这是必要的,因为当方法返回assertEquals时将使用该实现。否则,我们将不知道为什么对象被视为不相等。equalsfalse

This example also assumes that we have a reasonable toString implementation on our base class that prints out the values of the fields being compared. It is needed because assertEquals will use that implementation when the equals method returns false. Otherwise, we will have no idea of why the objects are considered unequal.

示例:状态公开子类

Example: State-Exposing Subclass

假设我们有以下测试,它要求Flight处于特定状态:

Suppose we have the following test, which requires a Flight to be in a particular state:

protected void setUp() 抛出异常 {

      super.setUp();

      schedulingFlight = createScheduledFlight();

}



Flight createScheduledFlight() 抛出 InvalidRequestException{

      Flight newFlight = new Flight();

      newFlight.schedule();

      返回 newFlight;

}



public void testDescchedule_shouldEndUpInUnscheduleState()

                             抛出异常 {

      schedulingFlight.deschedule();

      assertTrue("isUnsched", schedulingFlight.isUnscheduled());

}

protected  void  setUp()  throws  Exception  {

      super.setUp();

      scheduledFlight  =  createScheduledFlight();

}



Flight  createScheduledFlight()  throws  InvalidRequestException{

      Flight  newFlight  =  new  Flight();

      newFlight.schedule();

      return  newFlight;

}



public  void  testDeschedule_shouldEndUpInUnscheduleState()

                             throws  Exception  {

      scheduledFlight.deschedule();

      assertTrue("isUnsched",  scheduledFlight.isUnscheduled());

}

 

设置此测试的装置需要我们schedule在飞行中调用该方法:

Setting up the fixture for this test requires us to call the method schedule on the flight:

public class Flight{

      protected FlightState currentState = new UnscheduledState();



      /**

        * 将 Flight 从 <code>unscheduled</code>

        * 状态转换为 <code>scheduled</code> 状态。 * @throws InvalidRequestException 当        请求

        无效状态* 转换时        */       public void schedule() throws InvalidRequestException{             currentState.schedule();       } }











public  class  Flight{

      protected  FlightState  currentState  =  new  UnscheduledState();



      /**

        *  Transitions  the  Flight  from  the  <code>unscheduled</code>

        *  state  to  the  <code>scheduled</code>  state.

        *  @throws  InvalidRequestException  when  an  invalid  state

        *                  transition  is  requested

        */

      public  void  schedule()  throws  InvalidRequestException{

            currentState.schedule();

      }

}

 

该类Flight使用状态[GOF]模式,并将schedule方法的处理委托给当前引用的任何状态对象currentState。如果schedule尚未对的默认内容起作用,则此测试将在夹具设置期间失败currentState。我们可以通过使用状态公开子类来避免此问题,该子类提供了一种直接进入状态的方法,从而使其成为独立测试(参见第42页)。

The Flight class uses the State [GOF] pattern and delegates handling of the schedule method to whatever State object is currently referenced by currentState. This test will fail during fixture setup if schedule does not work yet on the default content of currentState. We can avoid this problem by using a State-Exposing Subclass that provides a method to move directly into the state, thereby making this an Independent Test (see page 42).

公共类 FlightTss 扩展了 Flight {



      public void becomeScheduled() {

            currentState = new ScheduledState();

      }

}

public  class  FlightTss  extends  Flight  {



      public  void  becomeScheduled()  {

            currentState  =  new  ScheduledState();

      }

}

 

通过在测试特定子类becomeScheduled中引入新方法,我们确保不会意外覆盖 SUT 的任何现有行为。现在我们要做的就是通过修改创建方法第 415页)在测试中实例化测试特定子类,而不是基类。

By introducing a new method becomeScheduled on the Test-Specific Subclass, we ensure that we will not accidentally override any existing behavior of the SUT. Now all we have to do is instantiate the Test-Specific Subclass in our test instead of the base class by modifying our Creation Method (page 415).

Flight createScheduledFlight() 抛出 InvalidRequestException{

    FlightTss newFlight = new FlightTss();

    newFlight.becomeScheduled();

    return newFlight;

}

Flight  createScheduledFlight()  throws  InvalidRequestException{

    FlightTss  newFlight  =  new  FlightTss();

    newFlight.becomeScheduled();

    return  newFlight;

}

 

Flight请注意,当我们实际上返回具有附加方法的测试特定子类的实例时,我们仍然声明我们正在返回该类的实例。

Note how we still declare that we are returning an instance of the Flight class when we are, in fact, returning an instance of the Test-Specific Subclass that has the additional method.

第24章

测试组织模式

Chapter 24

Test Organization Patterns

 

本章中的模式

Patterns in This Chapter

命名测试套件 592

Named Test Suite 592

测试代码重用

Test Code Reuse

      

测试实用程序方法 599

      

Test Utility Method 599

      

参数化测试 607

      

Parameterized Test 607

测试用例类结构

Testcase Class Structure

      

每个类的测试用例类 617

      

Testcase Class per Class 617

      

每个特性的测试用例类 624

      

Testcase Class per Feature 624

      

每个装置的测试用例类 631

      

Testcase Class per Fixture 631

实用程序方法位置

Utility Method Location

      

测试用例超类 638

      

Testcase Superclass 638

      

测试助手 643

      

Test Helper 643

命名测试套件

Named Test Suite

当我们需要运行任意组测试时,我们该如何运行测试?

How do we run the tests when we have arbitrary groups of tests to run?

我们定义了一个适当命名的测试套件,其中包含一组我们希望能够作为一组运行的测试。

We define a test suite, suitably named, that contains a set of tests that we wish to be able to run as a group.

图像

当我们有大量测试时,我们需要以系统的方式组织它们。测试套件允许我们将具有相关功能且彼此接近的测试分组。虽然我们希望能够轻松地运行整个应用程序或组件的所有测试,但我们也希望能够仅运行适用于系统功能或子组件的特定子集的测试。在其他情况下,我们希望仅运行我们定义的所有测试的子集。

When we have a large number of tests, we need to organize them in a systematic way. A test suite allows us to group tests that have related functionality close to each other. Although we want to be able to run all the tests for the entire application or component easily, we also want to be able to run only those tests applicable to specific subsets of the functionality or subcomponents of the system. In other situations, we want to run only a subset of all the tests we have defined.

命名测试套件为我们提供了一种选择要运行的预定义测试子集的方法。

Named Test Suites give us a way to choose which predefined subset of the tests we want to run.

工作原理

How It Works

对于我们希望能够作为一个组运行的每组相关测试,我们可以定义一个特殊的测试套件工厂(请参阅第399页的测试枚举),并赋予其一个意图揭示名称。工厂方法[GOF]可以使用多种测试套件构造技术中的任何一种来返回一个测试套件对象第 387页),该对象仅包含我们希望执行的特定测试用例对象第 382页)。

For each group of related tests that we would like to be able to run as a group, we can define a special Test Suite Factory (see Test Enumeration on page 399) with an Intent-Revealing Name. The Factory Method [GOF] can use any of several test suite construction techniques to return a Test Suite Object (page 387) containing only the specific Testcase Objects (page 382) we wish to execute.

何时使用它

When to Use It

虽然我们经常希望使用单个命令运行所有测试,但有时我们只想运行测试的子集。这样做最常见的原因是时间;为此,针对特定上下文运行AllTests Suite可能是我们最好的选择。当我们的 xUnit 成员不支持测试选择,并且我们要运行的测试分散在多个上下文中,并且某些上下文包含我们绝对不想运行的测试时,我们可以使用Subset Suite

Although we often want to run all the tests with a single command, sometimes we want to run only a subset of the tests. The most common reason for doing so is time; for this purpose, running the AllTests Suite for a specific context is probably our best bet. When our member of xUnit doesn't support Test Selection and the tests we want to run are scattered across multiple contexts and some contexts contain tests we definitely don't want run, we can use a Subset Suite.

变体:AllTests Suite

我们经常希望运行所有可用的测试。对于较小的系统,在签出新代码库后(以确保我们从已知点开始)和每次签入前(以确保所有代码都能正常工作)运行 AllTests Suite可能是标准做法。我们通常为每个软件包或软件命名空间准备一个AllTests Suite,这样我们就可以在每次代码更改后运行测试子集,作为“红-绿-重构”周期的一部分。

We often want to run all the tests we have available. With smaller systems, it may be standard practice to run the AllTests Suite after checking out a new code base (to ensure we start at a known point) and before every check-in (to ensure all our code works). We typically have an AllTests Suite for each package or namespace of software so that we can run subsets of the tests after each code change as part of the "red–green–refactor" cycle.

变体:子集套件

开发人员通常不想运行测试,因为它们是“慢速测试”第 253页)。测试访问数据库的组件的测试必然会比完全在内存中运行的测试运行得慢得多。通过为数据库测试定义一个命名测试套件,为内存测试定义另一个命名测试套件,我们可以选择不运行数据库测试,只需选择运行内存子集套件即可。

Developers often do not want to run tests because they are Slow Tests (page 253). Tests that exercise components that access a database will inevitably run much more slowly than tests that run entirely in memory. By defining one Named Test Suite for the database tests and another Named Test Suite for the in-memory tests, we can choose not to run the database tests simply by choosing to run the in-memory Subset Suite.

不运行测试的另一个常见原因是运行测试所需的上下文不可用。例如,如果我们的开发桌面上没有运行 Web 服务器,或者将软件部署到 Web 服务器需要太长时间,我们就不会运行需要 Web 服务器运行的组件的测试(它们只会花费额外的时间来运行,而且我们知道它们会失败并破坏我们获得绿条的机会)。

Another common reason given for not running tests is because the context they need to run is not available. For example, if we don't have a Web server running on our development desktop, or if deploying our software to the Web server takes too long, we won't want to run the tests of components that require the Web server to be running (they would just take extra time to run, and we know they will fail and spoil our chances of achieving a green bar).

变体:单一测试套件

子集套件的退化形式是单个测试套件,在其中我们实例化单个测试用例对象,以便可以运行单个测试方法(第 348页)。当我们没有可用的测试树资源管理器(请参阅第 377页的测试运行器)或测试方法需要某种形式的设置装饰器(第447页) 才能正常运行时,此变体特别有用。一些测试自动化程序始终在其工作区中打开“MyTest”测试用例类(第 373页),专门用于此目的。

The degenerate form of a Subset Suite is the Single Test Suite, in which we instantiate a single Testcase Object so that we can run a single Test Method (page 348). This variation is particularly useful when we don't have a Test Tree Explorer (see Test Runner on page 377) available or when the Test Method requires some form of Setup Decorator (page 447) to run properly. Some test automaters keep a "MyTest" Testcase Class (page 373) open in their workspace at all times specifically for this purpose.

实施说明

Implementation Notes

运行命名测试集的概念与我们如何构建命名测试套件无关。例如,我们可以使用测试枚举来明确构建测试套件,也可以使用测试发现第 393页)在特定位置(例如命名空间或程序集)查找所有测试。我们还可以在测试套件中执行测试选择第 403页),以动态创建较小的套件。xUnit 系列的一些成员要求我们手动为每个测试包或子系统定义AllTests 套件;其他成员,如 NUnit,会为每个命名空间自动创建一个测试套件对象。

The concept of running named sets of tests is independent of how we build the Named Test Suites. For example, we can use Test Enumeration to build up our suites of tests explicitly or we can use Test Discovery (page 393) to find all tests in a particular place (e.g., a namespace or assembly). We can also do Test Selection (page 403) from within a suite of tests to create a smaller suite dynamically. Some members of the xUnit family require us to define the AllTests Suites for each test package or subsystem manually; others, such as NUnit, automatically create a Test Suite Object for each namespace.

当我们使用测试枚举并为测试的各种子集指定测试套件时,最好根据这些子集来定义我们的AllTests 套件。当我们将AllTests 套件实现为套件的套件(参见测试套件对象)时我们只需向单个指定测试套件添加一个新的测试用例类;然后,此测试集合将汇总到本地上下文以及指定测试套件和下一个更高上下文的AllTests 套件中。

When we are using Test Enumeration and have Named Test Suites for various subsets of the tests, it is better to define our AllTests Suite in terms of these subsets. When we implement the AllTests Suite as a Suite of Suites (see Test Suite Object), we need to add a new Testcase Class to only a single Named Test Suite; this collection of tests is then rolled up into the AllTests Suite for the local context as well as the Named Test Suite and the next higher context.

重构说明

Refactoring Notes

将现有代码重构为命名测试套件的步骤高度依赖于我们使用的命名测试套件的变体。因此,我将省去激励示例,直接跳到命名测试套件的示例。

The steps to refactor existing code to a Named Test Suite are highly dependent on the variant of Named Test Suite we are using. For this reason, I'll dispense with the motivating example and skip directly to examples of Named Test Suites.

示例:AllTests Suite

Example: AllTests Suite

AllTests Suite可帮助我们针对所选功能的不同子集运行所有测试。对于每个子组件或上下文(例如 Java 包),我们定义一个名为 的特殊测试套件(及其对应的测试套件工厂AllTests。在测试套件工厂suite的工厂方法中我们添加当前上下文中的所有测试以及来自任何嵌套上下文(例如嵌套 Java 包)的所有命名测试套件。这样,当运行顶级命名测试套件时,嵌套上下文的所有命名测试套件也将运行。

An AllTests Suite helps us run all the tests for different subsets of the functionality of our choosing. For each subcomponent or context (e.g., a Java package), we define a special test suite (and its corresponding Test Suite Factory) called AllTests. In the suite Factory Method on the Test Suite Factory, we add all the tests in the current context and all the Named Test Suites from any nested contexts (such as nested Java packages). That way, when the top-level Named Test Suite is run, all Named Test Suites for the nested contexts will be run as well.

下面的示例说明了运行 xUnit 系列大多数成员中的所有测试所需的代码类型:

The following example illustrates the kind of code that would be required to run all the tests in most members of the xUnit family:

      公共类 AllTests {



            公共静态测试套件() {

            TestSuite suite = new TestSuite(“针对所有 JunitTests 进行测试”);

            //$JUnit-BEGIN$

            suite.addTestSuite(

                      com.clrstream.camug.example.test.InvoiceTest.class);

            suite.addTest(com.clrstream.ex7.test.AllTests.suite());

            suite.addTest(com.clrstream.ex8.test.AllTests.suite());

            suite.addTestSuite(

                      com.xunitpatterns.guardassertion.Example.class);

            //$JUnit-END$

            返回套件;

      }

}

      public  class  AllTests  {



            public  static  Test  suite()  {

            TestSuite  suite  =  new  TestSuite("Test  for  allJunitTests");

            //$JUnit-BEGIN$

            suite.addTestSuite(

                      com.clrstream.camug.example.test.InvoiceTest.class);

            suite.addTest(com.clrstream.ex7.test.AllTests.suite());

            suite.addTest(com.clrstream.ex8.test.AllTests.suite());

            suite.addTestSuite(

                      com.xunitpatterns.guardassertion.Example.class);

            //$JUnit-END$

            return  suite;

      }

}

 

在这种情况下,我们必须使用多种方法,因为我们要添加其他命名测试套件以及代表单个测试用例类的测试套件对象。在 JUnit 中,我们使用不同的方法来执行此操作。但是,xUnit 系列的其他成员可能使用相同的方法签名。

We had to use a mix of methods in this case because we are adding other Named Test Suites as well as Test Suite Objects representing a single Testcase Class. In JUnit, we use different methods to do this. Other members of the xUnit family, however, may use the same method signature.

此示例的另一个值得注意的方面是JUnit-startJUnit-end注释。IDE(在本例中为 Eclipse)通过自动重新生成这两个注释之间的列表来帮助我们——这是测试发现的半自动化形式。

The other notable aspect of this example is the JUnit-start and JUnit-end comments. The IDE (in this case, Eclipse) helps us out by automatically regenerating the list between these two comments—a semi-automated form of Test Discovery.

例如:特殊用途套件

Example: Special-Purpose Suite

假设我们有三个包含业务逻辑的主要包(A、B 和 C)。每个包都包含内存对象和数据库访问类。然后,我们将为这三个包中的每一个包设置相应的测试包。每个包中的一些测试需要数据库,而其他测试则可以完全在内存中运行。

Suppose we have three major packages (A, B, and C) containing business logic. Each package contains both in-memory objects and database access classes. We would then have corresponding test packages for each of the three packages. Some tests in each package would require the database, while others could run purely in memory.

我们希望能够对整个系统和每个包(A、B 和 C)运行以下测试集:

We want to be able to run the following sets of tests for the entire system, and for each package (A, B, and C):

  • 所有测试
  • All tests
  • 所有数据库测试
  • All database tests
  • 所有内存测试
  • All in-memory tests

这意味着总共有 12 个命名测试集(四个上下文中每个上下文有三个命名集)。

This implies a total of 12 named sets of tests (three named sets for each of four contexts).

在三个包(A、B 和 C)中,我们应该定义以下命名测试套件

In each of the three packages (A, B, and C), we should define the following Named Test Suites:

  • AllDbTests,通过添加所有包含数据库测试的测试用例类
  • AllDbTests, by adding all the Testcase Classes containing database tests
  • AllInMemoryTests,通过添加所有包含内存测试的测试用例类
  • AllInMemoryTests, by adding all the Testcase Classes containing in-memory tests
  • AllTests,通过结合AllDbTestsAllInMemoryTests
  • AllTests, by combining AllDbTests and AllInMemoryTests

然后,在顶层测试上下文中,我们使用相同的名称定义命名测试套件,如下所示:

Then, at the top-level testing context, we define Named Test Suites by the same names as follows:

  • AllDbTests,通过组合包 A、B 和 C 中的所有AllDbTests 测试用例类
  • AllDbTests, by composing all the AllDbTests Testcase Classes from packages A, B, and C
  • AllInMemoryTests,通过组合包 A、B 和 C 中的所有AllInMemoryTests 测试用例类
  • AllInMemoryTests, by composing all the AllInMemoryTests Testcase Classes from packages A, B, and C
  • AllTests,通过组合来自包 A、B 和 C 的所有AllTests 测试用例类(这只是普通的AllTests Suite。)
  • AllTests, by composing all the AllTests Testcase Classes from packages A, B, and C (This is just the normal AllTests Suite.)

如果我们发现需要在两个命名测试套件中的单个测试用例类中包含一些测试,则我们应该将该类拆分为针对每个上下文的一个类(例如,数据库测试和内存测试)。

If we find ourselves needing to include some tests from a single Testcase Class in both Named Test Suites, we should split the class into one class for each context (e.g., database tests and in-memory tests).

示例:单个测试套件

Example: Single Test Suite

在某些情况下(尤其是当我们使用调试器时),最好不要运行Testcase Class中的所有测试。仅运行这些测试的子集的一种方法是使用某些图形测试运行器提供的测试树资源管理器(请参阅测试运行器)。当此功能不可用时,一种常见的做法是禁用我们不想运行的测试,方法是将其注释掉,复制整个Testcase Class并删除大多数测试,或者更改测试的名称或属性,使它们被测试发现算法包括在内。

In some circumstances—especially when we are using a debugger—it is highly desirable to not run all the tests in a Testcase Class. One way to run only a subset of these tests is to use the Test Tree Explorer provided by some Graphical Test Runners (see Test Runner). When this capability isn't available, a common practice is to disable the tests we don't want run by either commenting them out, copying the entire Testcase Class and deleting most of the tests, or changing the names or attributes of the test that cause them to be included by the Test Discovery algorithm.

public class LostTests extends TestCase {

      public LostTests(String name) {

            super(name);

      }



      public void xtestOne() throws Exception {

            fail("测试未实现");

      }



      /*

      public void testTwo() throws Exception {

              fail("测试未实现");

      }

        */

      public void testSeventeen() throws Exception {

            assertTrue(true);

      }

}

public  class  LostTests  extends  TestCase  {

      public  LostTests(String  name)  {

            super(name);

      }



      public  void  xtestOne()  throws  Exception  {

            fail("test  not  implemented");

      }



      /*

      public  void  testTwo()  throws  Exception  {

              fail("test  not  implemented");

      }

        */

      public  void  testSeventeen()  throws  Exception  {

            assertTrue(true);

      }

}

 

如果在需要此测试策略的情况过去后,没有正确地逆转运行单个测试的方式,则所有这些方法都有可能丢失测试(请参阅第268页的生产错误)。单个测试套件可以运行特定的测试,而无需对相关的测试用例类进行任何更改。这种技术利用了这样一个事实:大多数 xUnit 实现都需要我们的测试用例类有一个参数的构造函数;这个参数由该类的实例将使用反射调用的方法名称组成。类中的每个测试方法都会调用一次单参数构造函数,并将生成的测试用例对象添加到测试套件对象中。(这是可插入行为[SBPP]模式的一个示例。)

All of these approaches suffer from the potential for Lost Tests (see Production Bugs on page 268) if the means of running a single test is not reversed properly when the situation requiring this testing strategy has passed. A Single Test Suite makes it possible to run the specific test(s) without making any changes to the Testcase Class in question. This technique takes advantage of the fact that most implementations of xUnit require a one-argument constructor on our Testcase Class; this argument consists of the name of the method that this instance of the class will invoke using reflection. The one-argument constructor is called once for each Test Method on the class, and the resulting Testcase Object is added to the Test Suite Object. (This is an example of the Pluggable Behavior [SBPP] pattern.)

我们可以通过实现一个测试套件工厂类来运行单个测试,该方法通过调用带有要运行的一个测试方法名称的单参数构造函数来创建所需测试用例类suite的实例。通过从 返回仅包含这一个测试用例对象的测试套件对象,我们无需触及目标测试用例类即可实现所需结果(运行单个测试)。suite

We can run a single test by implementing a Test Suite Factory class with a single method suite that creates an instance of the desired Testcase Class by calling the one-argument constructor with the name of the one Test Method to be run. By returning a Test Suite Object containing only this one Testcase Object from suite, we achieve the desired result (running a single test) without touching the target Testcase Class.

公共类 MyTest 扩展了 TestCase {



      公共静态测试套件() {

            返回新的 LostTests(“testSeventeen”);

      }

}

public  class  MyTest  extends  TestCase  {



      public    static  Test  suite()  {

            return  new  LostTests("testSeventeen");

      }

}

 

我喜欢始终保留单个测试套件suite类,只需通过更改导入语句和方法插入我想要运行的任何测试即可。通常,我会维护几个单个测试套件类,这样我就可以非常快速地在不同测试之间来回切换。我发现这种方法比在测试树资源管理器中深入研究并手动选择要运行的特定测试更容易。(您的里程可能会有所不同!)

I like to keep a Single Test Suite class around all the time and just plug in whatever test I want to run by changing the import statements and the suite method. Often, I maintain several Single Test Suite classes so I can flip back and forth between different tests very quickly. I find this technique easier to do than drilling down in the Test Tree Explorer and picking the specific test to run manually. (Your mileage may vary!)

示例:烟雾测试套件

Example: Smoke Test Suite

我们可以采用专用套件的理念,并将其与单一测试套件的实施技术相结合,以创建冒烟测试 [SCM] 套件。此策略涉及从系统的每个主要区域中挑选一两个代表性测试,并将这些测试包含在单个测试套件对象中。

We can take the idea of a Special-Purpose Suite and combine it with the implementation technique of a Single Test Suite to create a Smoke Test [SCM] suite. This strategy involves picking a representative test or two from each of the major areas of the system and including those tests in a single Test Suite Object.

public class SmokeTestSuite extends TestCase {

      public static Test suite() {

            TestSuite mySuite = new TestSuite("Smoke Tests");



            mySuite.addTest( new LostTests("testSeventeen") );

            mySuite.addTest( new SampleTests("testOne") );

            mySuite.addTest( new FlightManagementFacadeTest(

                  "testGetFlightsByOriginAirports_TwoOutboundFlights"));

            // 根据需要在此处添加其他测试...

            return mySuite;

      }

}

public  class  SmokeTestSuite  extends  TestCase  {

      public  static  Test  suite()  {

            TestSuite  mySuite  =  new  TestSuite("Smoke  Tests");



            mySuite.addTest(  new  LostTests("testSeventeen")  );

            mySuite.addTest(  new  SampleTests("testOne")            );

            mySuite.addTest(  new  FlightManagementFacadeTest(

                  "testGetFlightsByOriginAirports_TwoOutboundFlights"));

            //  add  additional  tests  here  as  needed...

            return  mySuite;

      }

}

 

这个方案不会彻底测试我们的系统,但它是一种快速找出核心功能的某些部分是否存在问题的方法。

This scheme won't test our system thoroughly, but it is a quick way to find out whether some part of the core functionality is broken.

测试实用程序方法

Test Utility Method

我们如何减少测试代码重复?

How do we reduce Test Code Duplication?

我们将想要重用的测试逻辑封装在一个适当命名的实用方法中。

We encapsulate the test logic we want to reuse behind a suitably named utility method.

图像

在编写测试时,我们总会发现自己需要在许多测试中重复相同的逻辑。最初,我们在编写需要相同逻辑的其他测试时,只会“克隆和调整”。然而,迟早我们会意识到这种测试代码重复(第 213页)开始造成问题。此时是考虑引入测试实用程序方法的好时机。

As we write tests, we will invariably find ourselves needing to repeat the same logic in many, many tests. Initially, we will just "clone and twiddle" as we write additional tests that need the same logic. Sooner or later, however, we will come to the realization that this Test Code Duplication (page 213) is starting to cause problems. This point is a good time to think about introducing a Test Utility Method.

工作原理

How It Works

子程序和函数是最早设计用来在程序中的多个位置重用逻辑的两种方法。测试实用方法正是将相同的原则应用于面向对象的测试代码。我们将出现在多个测试中的任何逻辑移到测试实用方法中;然后我们可以从各种测试中调用此方法,甚至可以在单个测试中多次调用此方法。当然,我们希望将任何因用途而异的东西作为参数传递给测试实用方法

The subroutine and the function were two of the earliest ways devised to reuse logic in several places within a program. A Test Utility Method is just the same principle applied to object-oriented test code. We move any logic that appears in more than one test into a Test Utility Method; we can then call this method from various tests or even several times from within a single test. Of course, we will want to pass in anything that varies from usage to usage as arguments to the Test Utility Method.

何时使用它

When to Use It

每当测试逻辑出现在多个测试中并且我们希望能够重用该逻辑时,我们就应该使用测试实用程序方法。我们还可以使用测试实用程序方法,因为我们想非常确定逻辑是否按预期工作。实现这种确定性的最佳方法是为可重用的测试逻辑编写自检测试(单元测试 - 参见第 26页)。由于测试方法第 348页)不易测试,因此最好将逻辑从测试方法中移出并移到测试实用程序方法中,这样更容易测试。

We should use a Test Utility Method whenever test logic appears in several tests and we want to be able to reuse that logic. We might also use a Test Utility Method because we want to be very sure that the logic works as expected. The best way to achieve that kind of certainty is to write Self-Checking Tests (unit tests—see page 26) for the reusable test logic. Because the Test Methods (page 348) cannot easily be tested, it is best to do this by moving the logic out of the test methods and into Test Utility Methods, where it can be more easily tested.

使用测试实用程序方法模式的主要缺点是它会创建另一个 API,测试自动化人员必须构建和理解该 API。通过对测试实用程序方法使用意图显示名称[SBPP]并使用重构作为定义测试实用程序方法的手段,可以大大减轻这种额外的工作量。

The main drawback of using the Test Utility Method pattern is that it creates another API that the test automaters must build and understand. This extra effort can be largely mitigated through the use of Intent-Revealing Names [SBPP] for the Test Utility Methods and through the use of refactoring as the means for defining the Test Utility Methods.

测试实用方法的种类与测试方法中的逻辑种类一样多。接下来,我们简要总结一些最流行的类型。其中一些变体非常重要,值得在本书的相应部分中撰写自己的模式说明。

There are as many different kinds of Test Utility Methods as there are kinds of logic in a Test Method. Next, we briefly summarize some of the most popular kinds. Some of these variations are important enough to warrant their own pattern write-ups in the corresponding section of this book.

变体:创建方法

创建方法第 415)用于创建可立即使用的对象作为夹具设置的一部分。它们隐藏了对象创建和相互依赖关系的复杂性。创建方法有足够多的变体,值得在其自己的部分中讨论此模式。

Creation Methods (page 415) are used to create ready-to-use objects as part of fixture setup. They hide the complexity of object creation and interdependencies from the test. Creation Method has enough variants to warrant addressing this pattern in its own section.

变化:连接方法

附件方法(参见创建方法)是创建方法的一种特殊形式,用于作为固定装置设置的一部分来修改已创建的对象。

An Attachment Method (see Creation Method) is a special form of Creation Method used to amend already-created objects as part of fixture setup.

变体:查找器方法

我们可以将检索共享装置(第 317页) 中的对象所需的任何逻辑封装在返回对象的函数中。然后我们为该函数赋予一个意图揭示名称,以便阅读测试的任何人都可以轻松理解我们在此测试中使用的装置。

We can encapsulate any logic required to retrieve objects from a Shared Fixture (page 317) within a function that returns the object(s). We then give this function an Intent-Revealing Name so that anyone reading the test can easily understand the fixture we are using in this test.

每当我们需要查找一个满足某些条件的现有共享夹具对象,并希望避免脆弱的夹具(请参阅第239页的脆弱测试)和高测试维护成本(第265页)时,都应该使用Finder方法。Finder方法既可以用于纯共享夹具策略,也可以用于混合策略,如不可变共享夹具(请参阅共享夹具)。Finder方法还可以通过封装如何找到所需对象以及确切使用哪些对象的机制来帮助防止模糊测试第 186页),从而使读者可以专注于理解为什么使用某个特定对象以及它与断言中描述的预期结果有何关系。这有助于我们走向“测试即文档”(请参阅​​第23页)。

We should use a Finder Method whenever we need to find an existing Shared Fixture object that meets some criteria and we want to avoid a Fragile Fixture (see Fragile Test on page 239) and High Test Maintenance Cost (page 265). Finder Methods can be used in either a pure Shared Fixture strategy or a hybrid strategy such as Immutable Shared Fixture (see Shared Fixture). Finder Methods also help prevent Obscure Tests (page 186) by encapsulating the mechanism of how the required objects are found and exactly which objects to use, thereby enabling the reader to focus on understanding why a particular object is being used and how it relates to the expected outcome described in the assertions. This helps us move toward Tests as Documentation (see page 23).

尽管大多数Finder 方法都返回单个对象引用,但该对象可能是对象树的根(例如,发票可能引用客户和各种地址,同时还包含一列项目)。在某些情况下,我们可以选择定义一个返回对象集合(或)的Finder 方法,但这种类型的Finder 方法的使用并不常见。Finder方法还可以更新参数以将其他对象传回调用它们的测试,尽管这种方法不像使用函数那样能揭示意图。我不建议通过初始化实例变量来传回对象,因为这很模糊,并且会妨碍我们稍后将Finder 方法移至测试助手第 643页)。ArrayHash

Although most Finder Methods return a single object reference, that object may be the root of a tree of objects (e.g., an invoice might refer to the customer and various addresses as well as containing a list of line items). In some circumstances, we may choose to define a Finder Method that returns a collection (Array or Hash) of objects, but the use of this type of Finder Method is less common. Finder Methods may also update parameters to pass additional objects back to the test that called them, although this approach is not as intent-revealing as use of a function. I do not recommend initialization of instance variables as a way of passing back objects because it is obscure and keeps us from moving the Finder Method to a Test Helper (page 643) later.

Finder方法可以通过多种方式在共享夹具中查找对象:使用直接引用(在夹具设置逻辑中初始化的实例变量或类变量)、使用已知键查找对象或使用特定条件搜索对象。使用直接引用或已知键的优点是每次运行测试时都返回完全相同的对象。主要缺点是其他某个测试可能已经修改了该对象,以致它不再符合Finder 方法名称所暗示的条件。按条件搜索可以避免此问题,但是如果每次运行时使用不同的对象,则生成的测试可能需要更长时间才能运行并且确定性可能更差。无论哪种方式,每当修改共享夹具时,我们必须在更少的地方修改代码(与在测试方法中直接使用对象时相比)。

The Finder Method can find objects in the Shared Fixture in several ways: by using direct references (instance variables or class variables initialized in the fixture setup logic), by looking the objects up using known keys, or by searching for the objects using specific criteria. Using direct references or known keys has the advantage of always returning exactly the same object each time the test is run. The main drawback is that some other test may have modified the object such that it may no longer match the criteria implied by the Finder Method's name. Searching by criteria can avoid this problem, though the resulting tests may take longer to run and might be less deterministic if they use different objects each time they are run. Either way, we must modify the code in fewer places whenever the Shared Fixture is modified (compared to when the objects are used directly within the Test Method).

变体:SUT 封装方法

也称为

Also known as

SUT API 封装

SUT API Encapsulation

使用测试实用程序方法的另一个原因是封装SUT API 的不必要知识。什么是不必要的?我们在 SUT 上调用的任何不是被测试方法的方法都会在测试和 SUT 之间产生额外的耦合。创建方法自定义断言第 474页)是SUT 封装方法的常见示例,值得将其作为单独的模式编写出来。本节重点介绍SUT 封装方法不太常见的用途。例如,如果我们正在执行的方法(或用于验证结果的方法)具有复杂的签名,那么编写和维护测试代码的工作量就会增加,并且可能使测试更难理解(模糊测试)。我们可以通过将这些调用包装在意图揭示且可能具有更简单签名的SUT 封装方法中来避免这个问题。

Another reason for using a Test Utility Method is to encapsulate unnecessary knowledge of the API of the SUT. What constitutes unnecessary? Any method we call on the SUT that is not the method being tested creates additional coupling between the test and the SUT. Creation Methods and Custom Assertions (page 474) are common enough examples of SUT Encapsulation Methods to warrant their own write-ups as separate patterns. This section focuses on the less common uses of SUT Encapsulation Methods. For example, if the method that we are exercising (or that we use for verifying the outcome) has a complicated signature, we increase the amount of work involved to write and maintain the test code and may make it harder to understand the tests (Obscure Test). We can avoid this problem by wrapping these calls in SUT Encapsulation Methods that are intent-revealing and may have simpler signatures.

变体:自定义断言

自定义断言用于以可在多个测试中重复使用的方式指定特定于测试的相等性。它们隐藏了将预期结果与实际结果进行比较的复杂性。自定义断言通常没有副作用,因为它们不与 SUT 交互来检索结果;该任务留给调用者。

Custom Assertions are used to specify test-specific equality in a way that is reusable across many tests. They hide the complexity of comparing the expected outcome with the actual outcome. Custom Assertions are typically free of side effects in that they do not interact with the SUT to retrieve the outcome; that task is left to the caller.

变体:验证方法

验证方法(参见自定义断言)用于验证预期结果是否发生。它们隐藏了测试中验证结果的复杂性。与自定义断言不同,验证方法与 SUT 交互。

Verification Methods (see Custom Assertion) are used to verify that the expected outcome has occurred. They hide the complexity of verifying the outcome from the test. Unlike Custom Assertions, Verification Methods interact with the SUT.

变体:参数化测试

测试实用程序方法模式最完整的形式是参数化测试第 607页)。从本质上讲,它是一种几乎完整的测试,可以在许多情况下重复使用。我们只需提供测试间不同的数据作为参数,然后让参数化测试为我们执行四阶段测试(第358页)的所有阶段。

The most complete form of the Test Utility Method pattern is the Parameterized Test (page 607). It is, in essence, an almost complete test that can be reused in many circumstances. We simply provide the data that varies from test to test as a parameter and let the Parameterized Test execute all the stages of the Four-Phase Test (page 358) for us.

变化:清理方法

清理方法1用于测试的夹具拆卸阶段,以清理测试结束后可能仍分配的任何资源。有关更详细的讨论和示例,请参阅模式自动拆卸第 503页)。

Cleanup Methods1 are used during the fixture teardown phase of the test to clean up any resources that might still be allocated after the test ends. Refer to the pattern Automated Teardown (page 503) for a more detailed discussion and examples.

实施说明

Implementation Notes

有些人对使用测试实用方法的主要反对意见是,这种模式会从测试中删除一些逻辑,从而导致测试更难阅读。在使用测试实用方法时,我们可以避免此问题的一种方法是赋予测试实用方法可揭示意图的名称。事实上,精心挑选的名称可以使测试更容易理解,因为它们通过定义用于定义测试的高级语言(参见第41页)来帮助防止模糊测试。保持测试实用方法相对较小且自包含也很有帮助。我们可以通过将所有参数明确地作为参数传递给这些方法(而不是使用实例变量),并将测试所需的任何对象作为明确的返回值或更新的参数返回来实现此目标。

The main objection some people have to using Test Utility Methods is that this pattern removes some of the logic from the test, which may make the test harder to read. One way we can avoid this problem when using Test Utility Methods is to give Intent-Revealing Names to the Test Utility Methods. In fact, well-chosen names can make the tests even easier to understand because they help prevent Obscure Tests by defining a Higher Level Language (see page 41) for defining tests. It is also helpful to keep the Test Utility Methods relatively small and self-contained. We can achieve this goal by passing all arguments to these methods explicitly as parameters (rather than using instance variables) and by returning any objects that the tests will require as explicit return values or updated parameters.

为了确保测试实用方法具有揭示意图的名称,我们应该让测试将测试实用方法引入其中,而不是仅仅发明我们认为以后可能需要的测试实用方法。这种“由外而内”的代码编写方法可以避免“明天再来找麻烦”,并帮助我们找到最小的解决方案。

To ensure that the Test Utility Methods have Intent-Revealing Names, we should let the tests pull the Test Utility Methods into existence rather than just inventing Test Utility Methods that we think may be needed later. This "outside-in" approach to writing code avoids "borrowing tomorrow's trouble" and helps us find the minimal solution.

编写可重用的测试实用程序方法相对简单。更棘手的问题是我们将把这个方法放在哪里。如果仅在单个测试用例类(第 373页) 中的测试方法中需要测试实用程序方法,那么我们可以将其放在该类中。但是,如果我们需要在几个类中使用测试实用程序方法,解决方案就会变得有点复杂。关键问题与类型可见性有关。客户端类需要能够看到测试实用程序方法测试实用程序方法需要能够看到它所依赖的所有类型和类。当它不依赖于许多类型/类,或者它所依赖的所有内容都可以从一个地方看到时,我们可以将测试实用程序方法放入我们为项目或公司定义的通用测试用例超类(第 638页)中。如果它依赖于无法从所有客户端都可以看到的一个地方看到的类型/类,那么我们可能需要将测试实用程序方法放在适当测试包或子系统中的测试助手上。在具有多组域对象(domain object)的大型系统中,通常做法是为每组相关域对象(包)配备一个测试助手。

Writing the reusable Test Utility Method is relatively straightforward. The trickier question is where we would put this method. If the Test Utility Method is needed only in Test Methods in a single Testcase Class (page 373), then we can put it onto that class. If we need the Test Utility Method in several classes, however, the solution becomes a bit more complicated. The key issue relates to type visibility. The client classes need to be able to see the Test Utility Method, and the Test Utility Method needs to be able to see all the types and classes on which it depends. When it doesn't depend on many types/classes or when everything it depends on is visible from a single place, we can put the Test Utility Method into a common Testcase Superclass (page 638) that we define for our project or company. If it depends on types/classes that cannot be seen from a single place that all the clients can see, then we may need to put the Test Utility Method on a Test Helper in the appropriate test package or subsystem. In larger systems with many groups of domain objects, it is common practice to have one Test Helper for each group (package) of related domain objects.

变体:测试实用程序测试

使用测试实用程序方法的一个主要优点是,原本不可测试的测试代码(请参阅第 209页的难以测试的代码)现在可以使用自检测试进行测试。此类测试的确切性质因所测试的测试实用程序方法的类型而异,但一个很好的例子是自定义断言测试(请参阅自定义断言)。

One major advantage of using Test Utility Methods is that otherwise Untestable Test Code (see Hard-to-Test Code on page 209) can now be tested with Self-Checking Tests. The exact nature of such tests varies based on the kind of Test Utility Method being tested but a good example is a Custom Assertion Test (see Custom Assertion).

激励人心的例子

Motivating Example

下面的示例展示的是许多新手测试自动化人员首先编写的测试:

The following example shows a test as many novice test automaters would first write it:

public void testAddItemQuantity_severalQuantity_v1(){

    地址 billingAddress = null;

    地址 shippingAddress = null;

    客户 customer = null;

    产品 product = null;

    发票 invoice = null;

    try {

          // 固定装置设置

          billingAddress = new Address("1222 1st St SW",

                                                            "Calgary", "Alberta",

                                                            "T2N 2V2", "Canada");

          shippingAddress = new Address("1333 1st St SW",

                                                              "Calgary", "Alberta",

                                                              "T2N 2V2", "Canada");

          customer = new Customer( 99, "John", "Doe",

                                                       new BigDecimal("30"),

                                                       billingAddress,

                                                       shippingAddress);

          product = new Product( 88, "SomeWidget",

                                                   new BigDecimal("19.99"));

          invoice = new Invoice( customer );

          // 练习 SUT

          invoice.addItemQuantity( product, 5 );

          // 验证结果

          列表 lineItems = invoice.getLineItems();

          if (lineItems.size() == 1) {

              LineItem actItem = (LineItem) lineItems.get(0);

              assertEquals("inv", invoice, actItem.getInv());

              assertEquals("prod", product, actItem.getProd());

              assertEquals("quant", 5, actItem.getQuantity());

              assertEquals("discount",

                                  new BigDecimal("30"),

                                  actItem.getPercentDiscount());

              assertEquals("unit price",

                                  new BigDecimal("19.99"),

                                  actItem.getUnitPrice());

              assertEquals("extended",

                                  new BigDecimal("69.96"),

                                  actItem.getExtendedPrice());

        } else {

              assertTrue("发票应有 1 个项目",false);

        }

    } 最后 {

        // 拆除

        deleteObject(invoice);

        deleteObject(product);

        deleteObject(customer);

        deleteObject(billingAddress);

        删除对象(运输地址);

    }

}

public  void  testAddItemQuantity_severalQuantity_v1(){

    Address  billingAddress  =  null;

    Address  shippingAddress  =  null;

    Customer  customer  =  null;

    Product  product  =  null;

    Invoice  invoice  =  null;

    try  {

          //      Fixture  Setup

          billingAddress  =  new  Address("1222  1st  St  SW",

                                                            "Calgary",  "Alberta",

                                                            "T2N  2V2",  "Canada");

          shippingAddress  =  new  Address("1333  1st  St  SW",

                                                              "Calgary",  "Alberta",

                                                              "T2N  2V2",  "Canada");

          customer  =  new  Customer(  99,  "John",  "Doe",

                                                       new  BigDecimal("30"),

                                                       billingAddress,

                                                       shippingAddress);

          product  =  new  Product(  88,  "SomeWidget",

                                                   new  BigDecimal("19.99"));

          invoice  =  new  Invoice(  customer  );

          //  Exercise  SUT

          invoice.addItemQuantity(  product,  5  );

          //  Verify  Outcome

          List  lineItems  =  invoice.getLineItems();

          if  (lineItems.size()  ==  1)  {

              LineItem  actItem  =  (LineItem)  lineItems.get(0);

              assertEquals("inv",  invoice,  actItem.getInv());

              assertEquals("prod",  product,  actItem.getProd());

              assertEquals("quant",  5,  actItem.getQuantity());

              assertEquals("discount",

                                  new  BigDecimal("30"),

                                  actItem.getPercentDiscount());

              assertEquals("unit  price",

                                  new  BigDecimal("19.99"),

                                  actItem.getUnitPrice());

              assertEquals("extended",

                                  new  BigDecimal("69.96"),

                                  actItem.getExtendedPrice());

        }  else  {

              assertTrue("Invoice  should  have  1  item",  false);

        }

    }  finally  {

        //  Teardown

        deleteObject(invoice);

        deleteObject(product);

        deleteObject(customer);

        deleteObject(billingAddress);

        deleteObject(shippingAddress);

    }

}

 

这个测试很难理解,因为它表现出许多代码异味,包括模糊测试硬编码测试数据(参见模糊测试)。

This test is difficult to understand because it exhibits many code smells, including Obscure Test and Hard-Coded Test Data (see Obscure Test).

重构说明

Refactoring Notes

在编写新测试时,我们经常通过挖掘现有测试中可重用逻辑来创建测试实用方法。我们可以使用提取方法 [Fowler] 重构将测试实用方法的代码从一个测试方法中提取出来,并将其作为测试实用方法放到测试用例类中。从那里,我们可以选择使用提取方法 [Fowler] 重构将测试实用方法移动到超类,或使用移动方法 [Fowler] 重构将其移动到其他类。

We often create Test Utility Methods by mining existing tests for reusable logic when we are writing new tests. We can use an Extract Method [Fowler] refactoring to pull the code for the Test Utility Method out of one Test Method and put it onto the Testcase Class as a Test Utility Method. From there, we may choose to move the Test Utility Method to a superclass by using a Pull Up Method [Fowler] refactoring or to another class by using a Move Method [Fowler] refactoring.

示例:测试实用程序方法

Example: Test Utility Method

这是早期测试的重构版本。请注意,与原始版本相比,此测试更容易理解。这只是使用测试实用方法可以实现的一个示例!

Here's the refactored version of the earlier test. Note how much simpler this test is to understand than the original version. And this is just one example of what we can achieve by using Test Utility Methods!

public void testAddItemQuantity_severalQuantity_v13(){

      final int QUANTITY = 5;

      final BigDecimal CUSTOMER_DISCOUNT = new BigDecimal("30");

      // 固定设置

      Customer customer =

            findActiveCustomerWithDiscount(CUSTOMER_DISCOUNT);

      Product product = findCurrentProductWith3DigitPrice( );

      Invoice invoice = createInvoice(customer);

      // 练习 SUT

      invoice.addItemQuantity(product, QUANTITY);

      // 验证结果

      final BigDecimal BASE_PRICE = product.getUnitPrice().

      multiply(new BigDecimal(QUANTITY));

      final BigDecimal EXTENDED_PRICE =

            BASE_PRICE.subtract(BASE_PRICE.multiply(

                                             CUSTOMER_DISCOUNT.movePointLeft(2)));

      LineItem 预期 =

                createLineItem( QUANTITY, CUSTOMER_DISCOUNT,

                                          EXTENDED_PRICE, 产品, 发票);

      assertContainsExactlyOneLineItem(发票, 预期);

}

public  void  testAddItemQuantity_severalQuantity_v13(){

      final  int  QUANTITY  =  5;

      final  BigDecimal  CUSTOMER_DISCOUNT  =  new  BigDecimal("30");

      //      Fixture  Setup

      Customer  customer  =

            findActiveCustomerWithDiscount(CUSTOMER_DISCOUNT);

      Product  product  =  findCurrentProductWith3DigitPrice(  );

      Invoice  invoice  =  createInvoice(customer);

      //  Exercise  SUT

      invoice.addItemQuantity(product,  QUANTITY);

      //  Verify  Outcome

      final  BigDecimal  BASE_PRICE  =  product.getUnitPrice().

      multiply(new  BigDecimal(QUANTITY));

      final  BigDecimal  EXTENDED_PRICE  =

            BASE_PRICE.subtract(BASE_PRICE.multiply(

                                             CUSTOMER_DISCOUNT.movePointLeft(2)));

      LineItem  expected  =

                createLineItem(  QUANTITY,  CUSTOMER_DISCOUNT,

                                          EXTENDED_PRICE,  product,  invoice);

      assertContainsExactlyOneLineItem(invoice,  expected);

}

 

Customer让我们一步一步地看一下这些变化。首先,我们将创建和的代码替换为ProductFinder 方法的调用,这些方法从不可变共享装置中检索这些对象我们以这种方式修改代码,因为我们不打算更改这些对象。

Let's go through the changes step by step. First, we replaced the code to create the Customer and the Product with calls to Finder Methods that retrieve those objects from an Immutable Shared Fixture. We altered the code in this way because we don't plan to change these objects.

受保护的客户 findActiveCustomerWithDiscount(

                                                       BigDecimal percentDiscount){

      返回CustomerHome.findCustomerById(

                                        ACTIVE_CUSTOMER_WITH_30PC_DISCOUNT_ID);

}

protected  Customer  findActiveCustomerWithDiscount(

                                                       BigDecimal  percentDiscount)  {

      return  CustomerHome.findCustomerById(

                                        ACTIVE_CUSTOMER_WITH_30PC_DISCOUNT_ID);

}

 

接下来,我们介绍了一种创建方法,我们计划在其中Invoice添加LineItem

Next, we introduced a Creation Method for the Invoice to which we plan to add the LineItem.

protected Invoice createInvoice(Customer customer) {

      Invoice newInvoice = new Invoice(customer);

      registerTestObject(newInvoice);

      返回 newInvoice;

}



列出 testObjects;

protected void registerTestObject(Object testObject) {

      testObjects.add(testObject);

}

protected  Invoice  createInvoice(Customer  customer)  {

      Invoice  newInvoice  =  new  Invoice(customer);

      registerTestObject(newInvoice);

      return  newInvoice;

}



List  testObjects;

protected  void  registerTestObject(Object  testObject)  {

      testObjects.add(testObject);

}

 

为了避免使用内联拆卸第 509页),我们使用自动拆卸机制注册了我们创建的每个对象,并从方法中调用该tearDown机制。

To avoid the need for In-line Teardown (page 509), we registered each of the objects we created with our Automated Teardown mechanism, which we call from the tearDown method.

private void deleteTestObjects() {

    Iterator i = testObjects.iterator();

    while (i.hasNext()) {

         try {

              deleteObject(i.next());

         } catch (RuntimeException e) {

               // 无需执行;我们只是想确保

               // 我们继续处理列表中的下一个对象。

         }

    }

}



public void teaDown() {

    deleteTestObjects();

}

private  void  deleteTestObjects()  {

    Iterator  i  =  testObjects.iterator();

    while  (i.hasNext())  {

         try  {

              deleteObject(i.next());

         }  catch  (RuntimeException  e)  {

               //  Nothing  to  do;  we  just  want  to  make  sure

               //  we  continue  on  to  the  next  object  in  the  list.

         }

    }

}



public  void  tearDown()  {

    deleteTestObjects();

}

 

最后,我们提取了一个自定义断言来验证是否LineItem已将正确的内容添加到Invoice

Finally, we extracted a Custom Assertion to verify that the correct LineItem has been added to the Invoice.

void assertContainsExactlyOneLineItem( Invoice invoice,

                                                                LineItem expected) {

     List lineItems = invoice.getLineItems();

     assertEquals("项目数量", lineItems.size(), 1);

     LineItem actItem = (LineItem)lineItems.get(0);

     assertLineItemsEqual("",expected, actItem);

}

void  assertContainsExactlyOneLineItem(  Invoice  invoice,

                                                                LineItem  expected)  {

     List  lineItems  =  invoice.getLineItems();

     assertEquals("number  of  items",  lineItems.size(),  1);

     LineItem  actItem  =  (LineItem)lineItems.get(0);

     assertLineItemsEqual("",expected,  actItem);

}

 

参数化测试

Parameterized Test

当相同的测试逻辑出现在许多测试中时,我们如何减少测试代码重复?

How do we reduce Test Code Duplication when the same test logic appears in many tests?

我们将执行夹具设置和结果验证所需的信息传递给实现整个测试生命周期的实用方法。

We pass the information needed to do fixture setup and result verification to a utility method that implements the entire test life cycle.

图像

测试可能非常重复,不仅因为我们必须一遍又一遍地运行相同的测试,还因为许多测试彼此之间只有细微的差别。例如,我们可能希望运行本质上相同的测试,但系统输入略有不同,并验证实际输出是否相应地变化。这些测试中的每一个都包含完全相同的步骤。虽然进行大量测试是确保良好代码覆盖率的绝佳方法,但从测试可维护性的角度来看,它并不那么有吸引力,因为对其中一个测试的算法所做的任何更改都必须传播到所有类似的测试。

Testing can be very repetitious not only because we must run the same test over and over again, but also because many of the tests differ only slightly from one another. For example, we might want to run essentially the same test with slightly different system inputs and verify that the actual output varies accordingly. Each of these tests would consist of the exact same steps. While having a large number of tests is an excellent way to ensure good code coverage, it is not so attractive from a test maintainability standpoint because any change made to the algorithm of one of the tests must be propagated to all similar tests.

参数化测试提供了一种在许多测试方法中重用相同测试逻辑的方法(第 348页)。

A Parameterized Test offers a way to reuse the same test logic in many Test Methods (page 348).

工作原理

How It Works

当然,解决方案是将通用逻辑分解成实用方法。当此逻辑包含整个四阶段测试第 358页)生命周期的所有四个部分(即夹具设置、练习 SUT、结果验证和夹具拆卸)时,我们将生成的实用方法称为参数化测试。这种测试以最少的代码维护为我们提供了最佳的覆盖率,并且可以非常轻松地在需要时添加更多测试。

The solution, of course, is to factor out the common logic into a utility method. When this logic includes all four parts of the entire Four-Phase Test (page 358) life cycle—that is, fixture setup, exercise SUT, result verification, and fixture teardown—we call the resulting utility method a Parameterized Test. This kind of test gives us the best coverage with the least code to maintain and makes it very easy to add more tests as they are needed.

如果我们有合适的实用方法,我们可以将原本需要一系列复杂步骤的测试简化为一行代码。当我们检测到测试之间的相似之处时,我们可以将共同点分解为测试实用方法第 599页),该方法仅将测试之间的不同信息作为其参数。测试方法将参数化测试运行所需的任何信息作为参数传递,这些信息因测试而异。

If the right utility method is available to us, we can reduce a test that would otherwise require a series of complex steps to a single line of code. As we detect similarities between our tests, we can factor out the commonalities into a Test Utility Method (page 599) that takes only the information that differs from test to test as its arguments. The Test Methods pass in as parameters any information that the Parameterized Test requires to run and that varies from test to test.

何时使用它

When to Use It

当多个测试实现相同的测试算法,但数据略有不同,并导致测试代码重复(第 213页)时,我们就可以使用参数化测试。不同的数据将成为传递给参数化测试的参数,而逻辑则由实用方法封装。参数化测试还可以帮助我们避免模糊测试(第 186页);通过减少重复相同逻辑的次数,它可以使测试用例类(第 373页) 更加紧凑。参数化测试也是数据驱动测试(第 288页) 的良好垫脚石;参数化测试的名称映射到数据驱动测试的动词或“动作词” ,参数则是属性。

We can use a Parameterized Test whenever Test Code Duplication (page 213) results from several tests implementing the same test algorithm but with slightly different data. The data that differs becomes the arguments passed to the Parameterized Test, and the logic is encapsulated by the utility method. A Parameterized Test also helps us avoid Obscure Tests (page 186); by reducing the number of times the same logic is repeated, it can make the Testcase Class (page 373) much more compact. A Parameterized Test is also a good steppingstone to a Data-Driven Test (page 288); the name of the Parameterized Test maps to the verb or "action word" of the Data-Driven Test, and the parameters are the attributes.

如果我们提取的实用方法不进行任何装置设置,则它被称为验证方法(请参阅第 474页的自定义断言)。如果它也不执行 SUT,则它被称为自定义断言

If our extracted utility method doesn't do any fixture setup, it is called a Verification Method (see Custom Assertion on page 474). If it also doesn't exercise the SUT, it is called a Custom Assertion.

实施说明

Implementation Notes

我们需要确保参数化测试具有一个意图揭示名称[SBPP],以便测试的读者能够理解它在做什么。这个名称应该暗示测试涵盖了整个生命周期,以避免任何混淆。一个惯例是名称以“test”开头或结尾;参数的存在传达了测试是参数化的事实。实现测试发现(第393页) 的 xUnit 家族的大多数成员将只为以“test”开头的“无参数”方法创建测试用例对象(第 382页),因此这个限制不应该阻止我们以“test”开头参数化测试名称。xUnit 家族中至少有一个成员 — MbUnit —在测试自动化框架(第 298页) 级别实现参数化测试。xUnit 家族的其他成员正在提供扩展,其中JUnit 的DDSteps是最早出现的扩展之一。

We need to ensure that the Parameterized Test has an Intent-Revealing Name [SBPP] so that readers of the test will understand what it is doing. This name should imply that the test encompasses the whole life cycle to avoid any confusion. One convention is to start or end the name in "test"; the presence of parameters conveys the fact that the test is parameterized. Most members of the xUnit family that implement Test Discovery (page 393) will create only Testcase Objects (page 382) for "no arg" methods that start with "test," so this restriction shouldn't prevent us from starting our Parameterized Test names with "test." At least one member of the xUnit family—MbUnit—implements Parameterized Tests at the Test Automation Framework (page 298) level. Extensions are becoming available for other members of the xUnit family, with DDSteps for JUnit being one of the first to appear.

测试狂热者会提倡编写自检测试(参见第 26页)来验证参数化测试。这样做的好处是显而易见的——包括增加我们对测试的信心——而且在大多数情况下这并不难。由于与 SUT 的交互,它比为自定义断言编写单元测试要难一些。我们可能需要用测试替身替换 SUT 2,这样我们就可以观察它是如何被调用的,并控制它返回的内容。

Testing zealots would advocate writing a Self-Checking Test (see page 26) to verify the Parameterized Test. The benefits of doing so are obvious—including increased confidence in our tests—and in most cases it isn't that hard to do. It is a bit harder than writing unit tests for a Custom Assertion because of the interaction with the SUT. We will likely need to replace the SUT2 with a Test Double so that we can observe how it is called and control what it returns.

变体:表格测试

也称为

Also known as

行测试

Row Test

本书的几位早期评论者写信给我,谈到了他们经常使用的参数化测试的一种变体:表格测试。这种测试的本质与参数化测试相同,只是整个值表位于一个测试方法中。不幸的是,这种方法使测试成为一种急切测试(参见第 224页的断言轮盘),因为它验证了许多测试条件。当所有测试都通过时,这个问题不是问题,但当其中一行失败时,它会导致缺乏缺陷定位(参见第 22页)。

Several early reviewers of this book wrote to me about a variation of Parameterized Test that they use regularly: the Tabular Test. The essence of this test is the same as that for a Parameterized Test, except that the entire table of values resides in a single Test Method. Unfortunately, this approach makes the test an Eager Test (see Assertion Roulette on page 224) because it verifies many test conditions. This issue isn't a problem when all of the tests pass, but it does lead to a lack of Defect Localization (see page 22) when one of the "rows" fails.

另一个潜在的问题是“行测试”可能会有意或无意地相互依赖,因为它们在同一个测试用例对象上运行;有关此行为的示例,请参阅增量表格测试。

Another potential problem is that "row tests" may depend on one another either on purpose or by accident because they are running on the same Testcase Object; see Incremental Tabular Test for an example of this behavior.

尽管存在这些潜在问题,表格测试仍然是一种非常有效的测试方法。xUnit 家族中至少有一个成员在框架级别实现了表格测试[RowTest]:MbUnit 提供了一个属性来指示测试是参数化测试,并提供了另一个属性[Row(x,y,...)]来指定要传递给它的参数。也许它会被移植到 xUnit 家族的其他成员?(提示,提示!)

Despite these potential issues, Tabular Tests can be a very effective way to test. At least one member of the xUnit family implements Tabular Tests at the framework level: MbUnit provides an attribute [RowTest] to indicate that a test is a Parameterized Test and another attribute [Row(x,y,...)] to specify the parameters to be passed to it. Perhaps it will be ported to other members of the xUnit family? (Hint, hint!)

变体:增量表格测试

增量表格测试表格测试模式的一种变体,我们特意在测试前几行留下的夹具上进行构建。它与一种特意形式的交互测试(请参见第 228页的不稳定测试)相同,称为链式测试(第454页),只是所有测试都位于同一个测试方法中。测试方法中的步骤有点像 Fit 中“DoFixture”的步骤,但没有单独报告失败的步骤。3

An Incremental Tabular Test is a variant of the Tabular Test pattern in which we deliberately build on the fixture left over by the previous rows of the test. It is identical to a deliberate form of Interacting Tests (see Erratic Test on page 228) called Chained Tests (page 454), except that all the tests reside within the same Test Method. The steps within the Test Method act somewhat like the steps of a "DoFixture" in Fit but without individual reporting of failed steps.3

变体:循环驱动测试

当我们想要使用特定列表或范围中的所有值来测试 SUT 时,我们可以从循环中调用参数化测试,该循环对列表或范围中的值进行迭代。通过在循环中嵌套循环,我们可以使用输入值组合来验证 SUT 的行为。执行此类测试的主要要求是,我们必须枚举每个输入值(或组合)的预期结果,或者使用计算值(请参阅第718页的派生值),而不在测试中引入生产逻辑(请参阅第 200页的条件测试逻辑)。然而,循环驱动测试存在与表格测试相同的许多问题,因为我们将许多测试隐藏在单个测试方法(因此也隐藏在测试用例对象)中。

When we want to test the SUT with all the values in a particular list or range, we can call the Parameterized Test from within a loop that iterates over the values in the list or range. By nesting loops within loops, we can verify the behavior of the SUT with combinations of input values. The main requirement for doing this type of testing is that we must either enumerate the expected result for each input value (or combination) or use a Calculated Value (see Derived Value on page 718) without introducing Production Logic in Test (see Conditional Test Logic on page 200). A Loop-Driven Test suffers from many of the same issues associated with a Tabular Test, however, because we are hiding many tests inside a single Test Method (and, therefore, Testcase Object).

激励人心的例子

Motivating Example

以下示例包括我在编写本书时用 Ruby 构建的网站发布基础架构中的一些 runit(Ruby Unit)测试。交叉引用标签的所有简单成功测试(参见测试方法)都经过了相同的步骤顺序:定义输入 XML、定义预期 HTML、桩输出文件、设置 XML 处理程序、提取结果 HTML 并将其与预期 HTML 进行比较。

The following example includes some of the runit (Ruby Unit) tests from the Web site publishing infrastructure I built in Ruby while writing this book. All of the Simple Success Tests (see Test Method) for my cross-referencing tags went through the same sequence of steps: defining the input XML, defining the expected HTML, stubbing out the output file, setting up the handler for the XML, extracting the resulting HTML, and comparing it with the expected HTML.

def test_extref

     # 设置

     sourceXml = "<extref id='abc'/>"

     expectedHtml = "<a href='abc.html'>abc</a>"

     mockFile = MockFile.new

     @handler = setupHandler(sourceXml, mockFile)

     # 执行

     @handler.printBodyContents

     # 验证

     assert_equals_html( expectedHtml, mockFile.output,

                                     "extref: html 输出")

结束



def testTestterm_normal

     sourceXml = "<testterm id='abc'/>"

     expectedHtml = "<a href='abc.html'>abc</a>"

     mockFile = MockFile.new

     @handler = setupHandler(sourceXml, mockFile)

     @handler.printBodyContents

     assert_equals_html( expectedHtml, mockFile.output,

                                     "testterm: html 输出")

结束



def testTestterm_plural

     sourceXml ="<testterms id='abc'/>"

     expectedHtml = "<a href='abc.html'>abcs</a>"

     mockFile = MockFile.new

     @handler = setupHandler(sourceXml, mockFile)

     @handler.printBodyContents

     assert_equals_html( expectedHtml, mockFile.output,

                                     "testterms: html 输出")

end

def  test_extref

     #  setup

     sourceXml  =  "<extref  id='abc'/>"

     expectedHtml  =  "<a  href='abc.html'>abc</a>"

     mockFile  =  MockFile.new

     @handler  =  setupHandler(sourceXml,  mockFile)

     #  execute

     @handler.printBodyContents

     #  verify

     assert_equals_html(  expectedHtml,  mockFile.output,

                                     "extref:  html  output")

end



def  testTestterm_normal

     sourceXml  =  "<testterm  id='abc'/>"

     expectedHtml  =  "<a  href='abc.html'>abc</a>"

     mockFile  =  MockFile.new

     @handler  =  setupHandler(sourceXml,  mockFile)

     @handler.printBodyContents

     assert_equals_html(  expectedHtml,  mockFile.output,

                                     "testterm:  html  output")

end



def  testTestterm_plural

     sourceXml  ="<testterms  id='abc'/>"

     expectedHtml  =  "<a  href='abc.html'>abcs</a>"

     mockFile  =  MockFile.new

     @handler  =  setupHandler(sourceXml,  mockFile)

     @handler.printBodyContents

     assert_equals_html(  expectedHtml,  mockFile.output,

                                     "testterms:  html  output")

end

 

尽管我们已经将许多通用逻辑分解到setupHandler方法中,但仍存在一些测试代码重复。就我而言,我至少有 20 个测试遵循相同的模式(还有更多测试正在进行中),因此我觉得让这些测试真正易于编写是值得的。

Even though we have already factored out much of the common logic into the setupHandler method, some Test Code Duplication remains. In my case, I had at least 20 tests that followed this same pattern (with lots more on the way), so I felt it was worthwhile to make these tests really easy to write.

重构说明

Refactoring Notes

重构为参数化测试与重构为自定义断言非常相似。主要区别在于,我们将对 SUT 的调用作为测试练习 SUT 阶段的一部分包含在我们应用提取方法 [Fowler] 重构的代码中。因为一旦我们定义了我们的装置和预期结果,这些测试实际上是相同的,所以其余的可以提取到参数化测试中。

Refactoring to a Parameterized Test is a lot like refactoring to a Custom Assertion. The main difference is that we include the calls to the SUT made as part of the exercise SUT phase of the test within the code to which we apply the Extract Method [Fowler] refactoring. Because these tests are virtually identical once we have defined our fixture and expected results, the rest can be extracted into the Parameterized Test.

示例:参数化测试

Example: Parameterized Test

在以下测试中,我们将每个测试简化为两个步骤:初始化两个变量并调用一个完成所有实际工作的实用方法。此实用方法是一个参数化测试

In the following tests, we have reduced each test to two steps: initializing two variables and calling a utility method that does all the real work. This utility method is a Parameterized Test.

def test_extref

        sourceXml = "<extref id='abc' />"

        expectedHtml = "<a href='abc.html'>abc</a>"

        generateAndVerifyHtml(sourceXml,expectedHtml,"<extref>")

end



def test_testterm_normal

      sourceXml = "<testterm id='abc'/>"

      expectedHtml = "<a href='abc.html'>abc</a>"

      generateAndVerifyHtml(sourceXml,expectedHtml,"<testterm>")

end



def test_testterm_plural

      sourceXml = "<testterms id='abc'/>"

      expectedHtml = "<a href='abc.html'>abcs</a>"

      generateAndVerifyHtml(sourceXml,expectedHtml,"<plural>")

end

def  test_extref

        sourceXml  =  "<extref  id='abc'  />"

        expectedHtml  =  "<a  href='abc.html'>abc</a>"

        generateAndVerifyHtml(sourceXml,expectedHtml,"<extref>")

end



def  test_testterm_normal

      sourceXml  =  "<testterm  id='abc'/>"

      expectedHtml  =  "<a  href='abc.html'>abc</a>"

      generateAndVerifyHtml(sourceXml,expectedHtml,"<testterm>")

end



def  test_testterm_plural

      sourceXml  =  "<testterms  id='abc'/>"

      expectedHtml  =  "<a  href='abc.html'>abcs</a>"

      generateAndVerifyHtml(sourceXml,expectedHtml,"<plural>")

end

 

通过定义参数化测试如下,可以实现这些测试的简洁性:

The succinctness of these tests is made possible by defining the Parameterized Test as follows:

def generateAndVerifyHtml( sourceXml, expectedHtml,

                                         message, &block)

     mockFile = MockFile.new

     sourceXml.delete!("\t")

     @handler = setupHandler(sourceXml, mockFile )

     block.call 除非 block == nil

     @handler.printBodyContents

     actual_html = mockFile.output

     assert_equal_html( expectedHtml,

                                   actual_html,

                                   message + "html output")

     actual_html

end

def  generateAndVerifyHtml(  sourceXml,  expectedHtml,

                                         message,  &block)

     mockFile  =  MockFile.new

     sourceXml.delete!("\t")

     @handler  =  setupHandler(sourceXml,  mockFile  )

     block.call  unless  block  ==  nil

     @handler.printBodyContents

     actual_html  =  mockFile.output

     assert_equal_html(  expectedHtml,

                                   actual_html,

                                   message  +  "html  output")

     actual_html

end

 

此参数化测试验证方法的区别在于,它包含四阶段测试的前三个阶段(从设置到验证),而验证方法仅执行练习 SUT 和验证结果阶段。请注意,我们的测试不需要拆卸阶段,因为我们使用的是垃圾收集拆卸第 500页)。

What distinguishes this Parameterized Test from a Verification Method is that it contains the first three phases of the Four-Phase Test (from setup to verify), whereas the Verification Method performs only the exercise SUT and verify result phases. Note that our tests did not need the teardown phase because we are using Garbage-Collected Teardown (page 500).

示例:独立表格测试

Example: Independent Tabular Test

以下是将相同测试编码为单个独立表格测试的示例:

Here's an example of the same tests coded as a single Independent Tabular Test:

def test_a_href_Generation

     row( “extref” ,”abc” ,”abc.html” ,”abc” )

     row( “testterm” ,'abc' ,”abc.html” ,”abc” )

     row( “testterms” ,'abc' ,”abc.html” ,”abcs”)

end



def row( tag, id, expected_href_id, expected_a_contents)

     sourceXml = “<” + tag + “ id='” + id + “'/>”

     expectedHtml = “<a href='” + expected_href_id + “'>”

                                                   + expected_a_contents + “</a>”

     msg = “<” + tag + "> “

     generateAndVerifyHtml( sourceXml, expectedHtml, msg)

end

def  test_a_href_Generation

     row(  "extref"     ,"abc","abc.html","abc"  )

     row(  "testterm"  ,'abc',"abc.html","abc"  )

     row(  "testterms",'abc',"abc.html","abcs")

end



def  row(  tag,  id,  expected_href_id,  expected_a_contents)

     sourceXml  =  "<"  +  tag  +  "  id='"  +  id  +  "'/>"

     expectedHtml  =  "<a  href='"  +  expected_href_id  +  "'>"

                                                   +  expected_a_contents  +  "</a>"

     msg  =  "<"  +  tag  +  ">  "

     generateAndVerifyHtml(  sourceXml,  expectedHtml,  msg)

end

 

这难道不是对各种测试条件的简洁、美观的表示吗?我只是对局部变量sourceXmlexpectedHtml参数列表进行了内联临时 [Fowler] 重构generateAndVerify,并将各种测试方法“混合”为一个。大部分工作涉及我们在现实生活中不必做的事情:将表格压缩以适应本书的页面宽度限制。这一限制迫使我缩短每行的文本,并在方法中重建 HTML 和预期的 XML row。我选择这个名字是row为了更好地将此示例与本节后面提供的 MbUnit 示例保持一致,但我也可以将其命名为其他名称,例如test_element

Isn't this a nice, compact representation of the various test conditions? I simply did an In-line Temp [Fowler] refactoring on the local variables sourceXml and expectedHtml in the argument list of generateAndVerify and "munged" the various Test Methods together into one. Most of the work involved something we won't have to do in real life: squeeze the table down to fit within the page-width limit for this book. That constraint forced me to abridge the text in each row and rebuild the HTML and the expected XML within the row method. I chose the name row to better align this example with the MbUnit example provided later in this section but I could have called it something else like test_element.

不幸的是,从测试运行器(第377页) 的角度来看,这是一个单一测试,与前面的示例不同。由于所有测试都位于同一个测试方法中,因此除最后一行之外的任何行中的失败都会导致信息丢失。在此示例中,我们不必担心交互测试,因为generateAndVerify每次调用它时都会构建一个新的测试装置。然而,在现实世界中,我们必须意识到这种可能性。

Unfortunately, from the Test Runner's (page 377) perspective, this is a single test, unlike the earlier examples. Because the tests all reside within the same Test Method, a failure in any row other than the last will cause a loss of information. In this example, we need not worry about Interacting Tests because generateAndVerify builds a new test fixture each time it is called. In the real world, however, we have to be aware of that possibility.

示例:增量表格测试

Example: Incremental Tabular Test

因为表格测试是在单个测试方法中定义的,所以它将在单个测试用例对象上运行。这为构建一系列操作提供了可能性。以下是 Clint Shank 在他的博客上提供的一个例子:

Because a Tabular Test is defined in a single Test Method, it will run on a single Testcase Object. This opens up the possibility of building up series of actions. Here's an example provided by Clint Shank on his blog:

public class TabularTest extends TestCase {

    private Order order = new Order();

    private static final double tolerance = 0.001;

    public void testGetTotal() {

        assertEquals("initial", 0.00, order.getTotal(), tolerance);

        testAddItemAndGetTotal("first", 1, 3.00, 3.00);

        testAddItemAndGetTotal("second",3, 5.00, 18.00);

        // 等等

    }



    private void testAddItemAndGetTotal( String msg,

                                                                  int lineItemQuantity,

                                                                  double lineItemPrice,

                                                                  double expectedTotal) {

        // 设置

        LineItem item = new LineItem( lineItemQuantity,

                                                                lineItemPrice);

        // 练习 SUT

        order.addItem(item);

        // 验证总计

        assertEquals(msg,expectedTotal,order.getTotal(),tolerance);

    }

}

public  class  TabularTest  extends  TestCase  {

    private  Order  order  =  new  Order();

    private  static  final  double  tolerance  =  0.001;

    public  void  testGetTotal()  {

        assertEquals("initial",  0.00,  order.getTotal(),  tolerance);

        testAddItemAndGetTotal("first",  1,  3.00,  3.00);

        testAddItemAndGetTotal("second",3,  5.00,  18.00);

        //  etc.

    }



    private  void  testAddItemAndGetTotal(  String  msg,

                                                                  int  lineItemQuantity,

                                                                  double  lineItemPrice,

                                                                  double  expectedTotal)  {

        //  setup

        LineItem  item  =  new  LineItem(      lineItemQuantity,

                                                                lineItemPrice);

        //  exercise  SUT

        order.addItem(item);

        //  verify  total

        assertEquals(msg,expectedTotal,order.getTotal(),tolerance);

    }

}

 

请注意增量表格测试的每一行是如何建立在上一行已经完成的内容之上的。

Note how each row of the Incremental Tabular Test builds on what was already done by the previous row.

示例:具有框架支持的表格测试(MbUnit)

Example: Tabular Test with Framework Support (MbUnit)

下面是来自 MbUnit 文档的一个示例,展示了如何使用[RowTest]属性来指示测试是参数化测试,以及如何使用另一个属性[Row(x,y,...)]来指定要传递给它的参数。

Here's an example from the MbUnit documentation that shows how to use the [RowTest] attribute to indicate that a test is a Parameterized Test and another attribute [Row(x,y,...)] to specify the parameters to be passed to it.

[RowTest()]

[Row(1,2,3)]

[Row(2,3,5)]

[Row(3,4,8)]

[Row(4,5,9)]

public void tAdd(Int32 x, Int32 y, Int32 expectedSum)

{

    Int32 Sum;

    Sum = this.Subject.Add(x,y);

    Assert.AreEqual(expectedSum, Sum);

}

[RowTest()]

[Row(1,2,3)]

[Row(2,3,5)]

[Row(3,4,8)]

[Row(4,5,9)]

public  void  tAdd(Int32  x,  Int32  y,  Int32  expectedSum)

{

    Int32  Sum;

    Sum  =  this.Subject.Add(x,y);

    Assert.AreEqual(expectedSum,  Sum);

}

 

除了属性的语法糖之外[Row(x,y,...)],此代码与上一个示例非常相似。但是,它不会因缺陷定位而受到影响,因为每行都被视为单独的测试。使用文本编辑器中的“查找和替换”功能将上一个示例转换为此格式是件很简单的事情。

Except for the syntactic sugar of the [Row(x,y,...)] attributes, this code sure looks similar to the previous example. It doesn't suffer from the loss of Defect Localization, however, because each row is considered a separate test. It would be a simple matter to convert the previous example to this format using the "find and replace" feature in a text editor.

示例:循环驱动测试(枚举值)

Example: Loop-Driven Test (Enumerated Values)

以下测试使用循环来用各种输入值集来测试 SUT:

The following test uses a loop to exercise the SUT with various sets of input values:

public void testMultipleValueSets() {

    // 设置夹具

    计算器 sut = new Calculator();

    TestValues[] testValues = {

                             new TestValues(1,2,3),

                             new TestValues(2,3,5),

                             new TestValues(3,4,8), // 特殊情况!

                             new TestValues(4,5,9)

                                                   };

    for (int i = 0; i < testValues.length; i++) {

        TestValues values = testValues[i];

        // 练习 SUT

        int actual = sut.calculate( values.a, values.b);

        // 验证结果

        assertEquals(message(i), values.expectedSum, actual);

    }

}



private String message(int i) {

    return "Row "+ String.valueOf(i);

}

public  void  testMultipleValueSets()  {

    //  Set  up  fixture

    Calculator  sut  =  new  Calculator();

    TestValues[]  testValues  =  {

                             new  TestValues(1,2,3),

                             new  TestValues(2,3,5),

                             new  TestValues(3,4,8),  //  special  case!

                             new  TestValues(4,5,9)

                                                   };

    for  (int  i  =  0;  i  <  testValues.length;  i++)  {

        TestValues  values  =  testValues[i];

        //  Exercise  SUT

        int  actual  =  sut.calculate(  values.a,  values.b);

        //  Verify  result

        assertEquals(message(i),  values.expectedSum,  actual);

    }

}



private  String  message(int  i)  {

    return  "Row  "+  String.valueOf(i);

}

 

在这种情况下,我们枚举了每组测试输入的预期值。此策略避免了测试中的生产逻辑

In this case we enumerated the expected value for each set of test inputs. This strategy avoids Production Logic in Test.

示例:循环驱动测试(计算值)

Example: Loop-Driven Test (Calculated Values)

下一个示例稍微复杂一些:

This next example is a bit more complex:

public void testCombinationsOfInputValues() {

    // 设置夹具

    计算器 sut = new Calculator();

    int expected; // 循环内有待解决



    for (int i = 0; i < 10; i++) {

         for (int j = 0; j < 10; j++) {

               // 练习 SUT

               int actual = sut.calculate( i, j );



               // 验证结果

               if (i==3 & j==4) // 特殊情况

                     expected = 8;

               else

                   expected = i+j;



               assertEquals(message(i,j), expected, actual);

         }

    }

}



private String message(int i, int j) {

    return "Cell( " + String.valueOf(i)+ ","

                            + String.valueOf(j) + ")";

}

public  void  testCombinationsOfInputValues()  {

    //  Set  up  fixture

    Calculator  sut  =  new  Calculator();

    int  expected;    //  TBD  inside  loops



    for  (int  i  =  0;  i  <  10;  i++)  {

         for  (int  j  =  0;  j  <  10;  j++)  {

               //  Exercise  SUT

               int  actual  =  sut.calculate(  i,  j  );



               //  Verify  result

               if  (i==3  &  j==4)    //  Special  case

                     expected  =  8;

               else

                   expected  =  i+j;



               assertEquals(message(i,j),  expected,  actual);

         }

    }

}



private  String  message(int  i,  int  j)  {

    return  "Cell(  "  +  String.valueOf(i)+  ","

                            +  String.valueOf(j)  +  ")";

}

 

不幸的是,由于需要处理特殊情况,它在测试中受到生产逻辑的影响。

Unfortunately, it suffers from Production Logic in Test because of the need to deal with the special case.

进一步阅读

[RowTest]有关和属性的更多信息,请参阅 MbUnit 的文档[Row()]。同样,有关 JUnit 的 DDSteps 扩展的描述,请参阅http://www.ddsteps.org ;虽然它的名称表明该工具支持数据驱动测试,但给出的示例是参数化测试。有关表格测试的更多参数,请参阅 Clint Shank 的博客,网址为http://clintshank.javadevelopersjournal.com/tabulartests.htm

See the documentation for MbUnit for more information on the [RowTest] and [Row()] attributes. Likewise, see http://www.ddsteps.org for a description of the DDSteps extension for JUnit; while its name suggests a tool that supports Data-Driven Testing, the examples given are Parameterized Tests. More arguments for Tabular Test can be found on Clint Shank's blog at http://clintshank.javadevelopersjournal.com/tabulartests.htm.

每个类的测试用例类

Testcase Class per Class

我们如何将测试方法组织到测试用例类中?

How do we organize our Test Methods onto Testcase Classes?

我们将一个 SUT 类的所有测试方法放入单个测试用例类中。

We put all the Test Methods for one SUT class onto a single Testcase Class.

图像

随着测试方法(第 348页)数量的增长,我们需要决定将每个测试方法放在哪个测试用例类(第 373页) 中。我们对测试组织策略的选择会影响我们获得测试“全局”视图的难易程度。它还会影响我们对夹具设置策略的选择。

As the number of Test Methods (page 348) grows, we need to decide on which Testcase Class (page 373) to put each Test Method. Our choice of a test organization strategy affects how easily we can get a "big picture" view of our tests. It also affects our choice of a fixture setup strategy.

每个类使用一个测试用例类是开始组织测试的简单方法。

Using a Testcase Class per Class is a simple way to start off organizing our tests.

工作原理

How It Works

我们为每个要测试的类创建一个单独的测试用例类。每个测试用例类都充当用于验证 SUT 类行为的所有测试方法的主页。

We create a separate Testcase Class for each class we wish to test. Each Testcase Class acts as a home to all the Test Methods that are used to verify the behavior of the SUT class.

何时使用它

When to Use It

当我们没有太多测试方法或刚开始为 SUT 编写测试时,使用每个类一个测试用例类是一个很好的起点。随着测试数量的增加以及我们对测试装置要求的了解加深,我们可能希望将测试用例类拆分为多个类。这种选择将导致每个装置一个测试用例类(第631页;如果我们的测试有少量常用的起点)或每个功能一个测试用例类(第624页;如果我们有几个不同的功能要测试)。正如 Kent Beck 所说,“让代码告诉你该做什么!”

Using a Testcase Class per Class is a good starting point when we don't have very many Test Methods or we are just starting to write tests for our SUT. As the number of tests increases and we gain a better understanding of our test fixture requirements, we may want to split the Testcase Class into multiple classes. This choice will result in either Testcase Class per Fixture (page 631; if we have a small number of frequently used starting points for our tests) or Testcase Class per Feature (page 624; if we have several distinct features to test). As Kent Beck would say, "Let the code tell you what to do!"

实施说明

Implementation Notes

选择测试用例类的名称非常简单:只需使用 SUT 类名,可能以“Test”作为前缀或后缀。方法名称应尝试至少捕获起始状态(装置)和正在执行的功能(方法),以及要传递给 SUT 的参数的摘要。考虑到这些要求,我们可能在方法名称中没有“空间”容纳预期结果,因此测试读取者必须查看测试方法主体以确定预期结果。

Choosing a name for the Testcase Class is pretty simple: Just use the SUT classname, possibly prefixed or suffixed with "Test." The method names should try to capture at least the starting state (fixture) and the feature (method) being exercised, along with a summary of the parameters to be passed to the SUT. Given these requirements, we likely won't have "room" for the expected outcome in the method name, so the test reader must look at the Test Method body to determine the expected outcome.

当使用每个类一个测试用例类时,创建 Fixture 是主要的实现问题。各种测试方法之间不可避免地会出现冲突的 Fixture 要求,这使得使用隐式设置(第424页)变得困难,并迫使我们使用内联设置第 408页)或委托设置第 411页)。第二个考虑因素是如何使 Fixture 的性质在每个测试方法中可见,以避免模糊测试第 186页)。除非内联设置非常简单,否则委托设置(使用创建方法;参见第415 )往往会产生更易读的测试。

The creation of the fixture is the primary implementation concern when using a Testcase Class per Class. Conflicting fixture requirements will inevitably arise among the various Test Methods, which makes use of Implicit Setup (page 424) difficult and forces us to use either In-line Setup (page 408) or Delegated Setup (page 411). A second consideration is how to make the nature of the fixture visible within each test method so as to avoid Obscure Tests (page 186). Delegated Setup (using Creation Methods; see page 415) tends to lead to more readable tests unless the In-line Setup is very simple.

示例:每个类的测试用例类

Example: Testcase Class per Class

下面是一个使用每个类一个测试用例类模式来为具有三种状态(、、和)和四种方法(、、、和)的类构建测试方法的示例。因为类是有状态的,所以我们需要对每种方法的每个状态进行至少一个测试。FlightUnscheduledScheduledAwaitingApprovalschedulerequestApprovaldeScheduleapprove

Here's an example of using the Testcase Class per Class pattern to structure the Test Methods for a Flight class that has three states (Unscheduled, Scheduled, and AwaitingApproval) and four methods (schedule, requestApproval, deSchedule, and approve. Because the class is stateful, we need at least one test for each state for each method.

public class FlightStateTest 扩展了 TestCase {



      public void testRequestApproval_FromScheduledState() 抛出异常 {

            Flight flight = FlightTestHelper.getAnonymousFlightInScheduledState();

      try {

            flight.requestApproval();

            fail("不允许处于预定状态");

      } catch (InvalidRequestException e) {

           assertEquals("InvalidRequestException.getRequest()",

                               "requestApproval",

                               e.getRequest());

           assertTrue("isScheduled()", flight.isScheduled());

      }

    }



      public void testRequestApproval_FromUnsheduledState()

                                  抛出异常 {

            Flight flight = FlightTestHelper.getAnonymousFlightInUnscheduledState

                                  ();

            flight.requestApproval();

            assertTrue("isAwaitingApproval()",

                             flight.isAwaitingApproval());

      }



      public void testRequestApproval_FromAwaitingApprovalState()

                           抛出异常 {

            Flight flight = FlightTestHelper.

                                  getAnonymousFlightInAwaitingApprovalState();

            try {

                 flight.requestApproval();

                 fail("不允许处于 awaitingApproval 状态");

            } catch (InvalidRequestException e) {

                 assertEquals("InvalidRequestException.getRequest()",

                                     "requestApproval",

                                     e.getRequest());

             assertTrue("isAwaitingApproval()",

                                     flight.isAwaitingApproval());

            }

      }



      public void testSchedule_FromUnscheduledState()

                                 抛出异常 {

            Flight flight = FlightTestHelper.

                                       getAnonymousFlightInUnscheduledState();

            flight.schedule();

            assertTrue( "isScheduled()", flight.isScheduled());

      }



      public void testSchedule_FromScheduledState()

                               抛出异常 {

          Flight flight = FlightTestHelper.getAnonymousFlightInScheduledState

          ();

          尝试 {

              flight.schedule();

              失败(“不允许处于预定状态”);

              } catch(InvalidRequestException e){

                    assertEquals(“InvalidRequestException.getRequest()”,

                                       “schedule”,

                                       e.getRequest());

                    assertTrue(“isScheduled()”,flight.isScheduled());

            }

      }



      public void testSchedule_FromAwaitingApprovalState()

                                抛出异常 {

          Flight flight = FlightTestHelper。getAnonymousFlightInAwaitingApprovalState

                                     ();

          尝试 {

                flight.schedule();

                失败(“不允许处于预定状态”);

          } catch(InvalidRequestException e){

                assertEquals(“InvalidRequestException.getRequest()”,

                                    “schedule”,

                                    e.getRequest());

                  assertTrue(“isAwaitingApproval()”,

                                    flight.isAwaitingApproval();

            }

      }



      public void testDescchedule_FromScheduledState()

                                 抛出异常 {

            Flight flight = FlightTestHelper。

                                  getAnonymousFlightInScheduledState();

            flight.deschedule();

            assertTrue("isUnscheduled()", flight.isUnscheduled());

      }



      public void testDescchedule_FromUnscheduledState()

                                  throws Exception {

            Flight flight = FlightTestHelper.

                                 getAnonymousFlightInUnscheduledState();

            try {

                flight.deschedule();

                fail("不允许处于未计划状态");

            } catch (InvalidRequestException e) {

                  assertEquals("InvalidRequestException.getRequest()",

                                      "deschedule",

                                      e.getRequest());

                  assertTrue("isUnscheduled()", flight.isUnscheduled());

            }

      }



      public void testDescchedule_FromAwaitingApprovalState()

                                  throws Exception {

            Flight flight = FlightTestHelper.

                                       getAnonymousFlightInAwaitingApprovalState();

            尝试{

                  flight.deschedule();

                  fail("不允许处于 awaitingApproval 状态");

            } catch (InvalidRequestException e) {

                  assertEquals("InvalidRequestException.getRequest()",

                                      "deschedule",

                                      e.getRequest());

                  assertTrue( "isAwaitingApproval()",

                                     flight.isAwaitingApproval());

            }

      }



      public void testApprove_FromScheduledState()

                                throws Exception {

            Flight flight = FlightTestHelper.

                                       getAnonymousFlightInScheduledState();

            try {

                  flight.approve("Fred");

                  fail("不允许处于 Scheduled 状态");

            } catch (InvalidRequestException e) {

                  assertEquals("InvalidRequestException.getRequest()",

                                      "approve",

                                      e.getRequest());

                    assertTrue("isScheduled()", flight.isScheduled());

            }

      }



      public void testApprove_FromUnsheduledState()

                               throws Exception {

            Flight flight = FlightTestHelper.

                                       getAnonymousFlightInUnscheduledState();

            尝试 {

                  flight.approve("Fred");

                  失败("不允许处于未计划状态");

            } catch (InvalidRequestException e) {

                  assertEquals("InvalidRequestException.getRequest()",

                                      "approve",

                                      e.getRequest());

                  assertTrue( "isUnscheduled()", flight.isUnscheduled());

            }

      }



      public void testApprove_FromAwaitingApprovalState()

                                  抛出异常 {

            Flight flight = FlightTestHelper。

                                      getAnonymousFlightInAwaitingApprovalState();

            flight.approve("Fred");

            assertTrue("isScheduled()", flight.isScheduled());

      }



      public void testApprove_NullArgument() 抛出异常 {

            Flight flight = FlightTestHelper。

                                        获取匿名航班正在等待批准状态();

            尝试 {

                  flight.approve(null);

                  失败("未能捕获审批人");

            } 捕获 (InvalidArgumentException e) {

                  assertEquals("e.getArgumentName()",

                                      "approverName", e.getArgumentName());

                  assertNull( "e.getArgumentValue()",

                                      e.getArgumentValue());

                  assertTrue( "isAwaitingApproval()",

                                     flight.isAwaitingApproval());

            }

      }



      public void testApprove_InvalidApprover() 抛出异常 {

            Flight flight = FlightTestHelper.

                                       getAnonymousFlightInAwaitingApprovalState();

            尝试 {

                  flight.approve("John");

                  失败("未能验证审批人");

            } 捕获 (InvalidArgumentException e) {

                  assertEquals("e.getArgumentName()",

                                "approverName",

                                e.getArgumentName());

            断言Equals("e.getArgumentValue()",

                                "John",

                                e.getArgumentValue());

            断言True( "isAwaitingApproval()",

                                 flight.isAwaitingApproval());

      }

   }

}

public  class  FlightStateTest  extends  TestCase  {



      public  void  testRequestApproval_FromScheduledState()  throws  Exception  {

            Flight  flight  =  FlightTestHelper.getAnonymousFlightInScheduledState();

      try  {

            flight.requestApproval();

            fail("not  allowed  in  scheduled  state");

      }  catch  (InvalidRequestException  e)  {

           assertEquals("InvalidRequestException.getRequest()",

                               "requestApproval",

                               e.getRequest());

           assertTrue("isScheduled()",  flight.isScheduled());

      }

    }



      public  void  testRequestApproval_FromUnsheduledState()

                                  throws  Exception  {

            Flight  flight  =  FlightTestHelper.

                                  getAnonymousFlightInUnscheduledState();

            flight.requestApproval();

            assertTrue("isAwaitingApproval()",

                             flight.isAwaitingApproval());

      }



      public  void  testRequestApproval_FromAwaitingApprovalState()

                           throws  Exception  {

            Flight  flight  =  FlightTestHelper.

                                  getAnonymousFlightInAwaitingApprovalState();

            try  {

                 flight.requestApproval();

                 fail("not  allowed  in  awaitingApproval  state");

            }  catch  (InvalidRequestException  e)  {

                 assertEquals("InvalidRequestException.getRequest()",

                                     "requestApproval",

                                     e.getRequest());

             assertTrue("isAwaitingApproval()",

                                     flight.isAwaitingApproval());

            }

      }



      public  void  testSchedule_FromUnscheduledState()

                                 throws  Exception  {

            Flight  flight  =  FlightTestHelper.

                                       getAnonymousFlightInUnscheduledState();

            flight.schedule();

            assertTrue(  "isScheduled()",  flight.isScheduled());

      }



      public  void  testSchedule_FromScheduledState()

                               throws  Exception  {

          Flight  flight  =  FlightTestHelper.

          getAnonymousFlightInScheduledState();

          try  {

              flight.schedule();

              fail("not  allowed  in  scheduled  state");

              }  catch  (InvalidRequestException  e)  {

                    assertEquals("InvalidRequestException.getRequest()",

                                       "schedule",

                                       e.getRequest());

                    assertTrue("isScheduled()",  flight.isScheduled());

            }

      }



      public  void  testSchedule_FromAwaitingApprovalState()

                                throws  Exception  {

          Flight  flight  =  FlightTestHelper.

                                     getAnonymousFlightInAwaitingApprovalState();

          try  {

                flight.schedule();

                fail("not  allowed  in  scheduled  state");

          }  catch  (InvalidRequestException  e)  {

                assertEquals("InvalidRequestException.getRequest()",

                                    "schedule",

                                    e.getRequest());

                  assertTrue(  "isAwaitingApproval()",

                                    flight.isAwaitingApproval());

            }

      }



      public  void  testDeschedule_FromScheduledState()

                                 throws  Exception  {

            Flight  flight  =  FlightTestHelper.

                                  getAnonymousFlightInScheduledState();

            flight.deschedule();

            assertTrue("isUnscheduled()",  flight.isUnscheduled());

      }



      public  void  testDeschedule_FromUnscheduledState()

                                  throws  Exception  {

            Flight  flight  =  FlightTestHelper.

                                 getAnonymousFlightInUnscheduledState();

            try  {

                flight.deschedule();

                fail("not  allowed  in  unscheduled  state");

            }  catch  (InvalidRequestException  e)  {

                  assertEquals("InvalidRequestException.getRequest()",

                                      "deschedule",

                                      e.getRequest());

                  assertTrue("isUnscheduled()",  flight.isUnscheduled());

            }

      }



      public  void  testDeschedule_FromAwaitingApprovalState()

                                  throws  Exception  {

            Flight  flight  =  FlightTestHelper.

                                       getAnonymousFlightInAwaitingApprovalState();

            try  {

                  flight.deschedule();

                  fail("not  allowed  in  awaitingApproval  state");

            }  catch  (InvalidRequestException  e)  {

                  assertEquals("InvalidRequestException.getRequest()",

                                      "deschedule",

                                      e.getRequest());

                  assertTrue(    "isAwaitingApproval()",

                                     flight.isAwaitingApproval());

            }

      }



      public  void  testApprove_FromScheduledState()

                                throws  Exception  {

            Flight  flight  =  FlightTestHelper.

                                       getAnonymousFlightInScheduledState();

            try  {

                  flight.approve("Fred");

                  fail("not  allowed  in  scheduled  state");

            }  catch  (InvalidRequestException  e)  {

                  assertEquals("InvalidRequestException.getRequest()",

                                      "approve",

                                      e.getRequest());

                    assertTrue("isScheduled()",  flight.isScheduled());

            }

      }



      public  void  testApprove_FromUnsheduledState()

                               throws  Exception  {

            Flight  flight  =  FlightTestHelper.

                                       getAnonymousFlightInUnscheduledState();

            try  {

                  flight.approve("Fred");

                  fail("not  allowed  in  unscheduled  state");

            }  catch  (InvalidRequestException  e)  {

                  assertEquals("InvalidRequestException.getRequest()",

                                      "approve",

                                      e.getRequest());

                  assertTrue(  "isUnscheduled()",  flight.isUnscheduled());

            }

      }



      public  void  testApprove_FromAwaitingApprovalState()

                                  throws  Exception  {

            Flight  flight  =  FlightTestHelper.

                                      getAnonymousFlightInAwaitingApprovalState();

            flight.approve("Fred");

            assertTrue("isScheduled()",  flight.isScheduled());

      }



      public  void  testApprove_NullArgument()  throws  Exception  {

            Flight  flight  =  FlightTestHelper.

                                        getAnonymousFlightInAwaitingApprovalState();

            try  {

                  flight.approve(null);

                  fail("Failed  to  catch  no  approver");

            }  catch  (InvalidArgumentException  e)  {

                  assertEquals("e.getArgumentName()",

                                      "approverName",  e.getArgumentName());

                  assertNull(    "e.getArgumentValue()",

                                      e.getArgumentValue());

                  assertTrue(    "isAwaitingApproval()",

                                     flight.isAwaitingApproval());

            }

      }



      public  void  testApprove_InvalidApprover()  throws  Exception  {

            Flight  flight  =  FlightTestHelper.

                                       getAnonymousFlightInAwaitingApprovalState();

            try  {

                  flight.approve("John");

                  fail("Failed  to  validate  approver");

            }  catch  (InvalidArgumentException  e)  {

                  assertEquals("e.getArgumentName()",

                                "approverName",

                                e.getArgumentName());

            assertEquals("e.getArgumentValue()",

                                "John",

                                e.getArgumentValue());

            assertTrue(    "isAwaitingApproval()",

                                 flight.isAwaitingApproval());

      }

   }

}

 

此示例使用委托设置新装置(第311页)来实现更具声明性的装置构造风格。即便如此,这个类也变得相当大,跟踪测试方法变得有点繁琐。即使我们的 IDE 提供的“大图”也没有那么清晰;我们可以看到正在执行的测试条件,但如果不查看方法主体,就无法知道预期结果应该是什么(图 24.1)。

This example uses Delegated Setup of a Fresh Fixture (page 311) to achieve a more declarative style of fixture construction. Even so, this class is getting rather large and keeping track of the Test Methods is becoming a bit of a chore. Even the "big picture" provided by our IDE is not that illuminating; we can see the test conditions being exercised but cannot tell what the expected outcome should be without looking at the method bodies (Figure 24.1).

图 24.1. Eclipse IDE 的 Package Explorer 中显示的测试用例类示例。请注意起始状态和事件都包含在测试方法名称中。

Figure 24.1. Testcase Class per Class example as seen in the Package Explorer of the Eclipse IDE. Note how both the starting state and event are included in the Test Method names.

图像

每个功能的测试用例类

Testcase Class per Feature

我们如何将测试方法组织到测试用例类中?

How do we organize our Test Methods onto Testcase Classes?

我们根据测试方法所执行的 SUT 的可测试特性,将测试方法分组到测试用例类中。

We group the Test Methods onto Testcase Classes based on which testable feature of the SUT they exercise.

图像

随着测试方法(第 348页)数量的增长,我们需要决定将每个测试方法放在哪个测试用例类(第 373页) 中。我们对测试组织策略的选择会影响我们获得测试“全局”视图的难易程度。它还会影响我们对夹具设置策略的选择。

As the number of Test Methods (page 348) grows, we need to decide on which Testcase Class (page 373) to put each Test Method. Our choice of a test organization strategy affects how easily we can get a "big picture" view of our tests. It also affects our choice of a fixture setup strategy.

每个功能使用一个测试用例类为我们提供了一种系统的方法,可以将大型测试用例类分解为几个较小的类,而无需更改我们的测试方法

Using a Testcase Class per Feature gives us a systematic way to break up a large Testcase Class into several smaller ones without having to change our Test Methods.

工作原理

How It Works

我们根据测试方法验证的测试用例类的功能,将测试方法分组到测试用例中。这种组织方案允许我们拥有更小的测试用例类,并且可以一目了然地查看类中特定功能的所有测试条件。

We group our Test Methods onto Testcase Classes based on which feature of the Testcase Class they verify. This organizational scheme allows us to have smaller Testcase Classes and to see at a glance all the test conditions for a particular feature of the class.

何时使用它

When to Use It

当我们有大量测试方法,并且希望使 SUT 的每个特性的规范更加明显时,我们可以使用每个特性一个测试用例类。不幸的是,每个特性一个测试用例类并不能使每个单独的测试方法变得更简单或更容易理解;只有每个装置一个测试用例类(第631页) 在这方面有所帮助。同样,当 SUT 的每个特性只需要一两个测试时,使用每个特性一个测试用例类也没有多大意义;在这种情况下,我们可以坚持使用每个类一个测试用例类(第 617页)。

We can use a Testcase Class per Feature when we have a significant number of Test Methods and we want to make the specification of each feature of the SUT more obvious. Unfortunately, Testcase Class per Feature does not make each individual Test Method any simpler or easier to understand; only Testcase Class per Fixture (page 631) helps on that front. Likewise, it doesn't make much sense to use Testcase Class per Feature when each feature of the SUT requires only one or two tests; in that case, we can stick with a single Testcase Class per Class (page 617).

请注意,类中有大量功能是一种“异味”,表明该类可能承担了太多职责。当我们为服务外观[GOF]上的方法编写客户测试时,我们通常使用每个功能一个测试用例类

Note that having a large number of features on a class is a "smell" indicating the possibility that the class might have too many responsibilities. We typically use Testcase Class per Feature when we are writing customer tests for methods on a service Facade [GOF].

变体:每个方法的测试用例类

当一个类的方法采用大量不同的参数时,我们可能对该方法进行多次测试。我们可以将所有这些测试方法分组到每个方法的单个测试用例类中,并将其余测试方法放在一个或多个其他测试用例类中。

When a class has methods that take a lot of different parameters, we may have many tests for the one method. We can group all of these Test Methods onto a single Testcase Class per Method and put the rest of the Test Methods onto one or more other Testcase Classes.

变体:每个特性的测试用例类

虽然类的“特性”通常是一个单一的操作或功能,但它也可能是一组对对象的同一实例变量进行操作的相关方法。例如, Java Bean 的setget方法将被视为包含这些方法的类的单一(且微不足道的)“特性”。类似地,数据访问对象[CJ2EEP]将提供读取和写入对象的方法。很难单独测试这些方法,因此我们可以将一种对象的读取和写入视为一种特性。

Although a "feature" of a class is typically a single operation or function, it may also be a set of related methods that operate on the same instance variable of the object. For example, the set and get methods of a Java Bean would be considered a single (and trivial) "feature" of the class that contains those methods. Similarly, a Data Access Object [CJ2EEP] would provide methods to both read and write objects. It is difficult to test these methods in isolation, so we can treat the reading and writing of one kind of object as a feature.

变化:每个用户故事的测试用例类

如果我们正在进行高度增量开发(例如,我们可能会使用极限编程),将每个故事的新测试方法放入不同的测试用例类中会很有用。当不同的人处理影响同一 SUT 类的不同故事时,这种做法可以防止与提交相关的冲突。每个用户故事的测试用例类模式最终可能会与每个功能的测试用例类每个方法的测试用例类相同,也可能不同,这取决于我们如何划分用户故事。

If we are doing highly incremental development (such as we might do with eXtreme Programming), it can be useful to put the new Test Methods for each story into a different Testcase Class. This practice prevents commit-related conflicts when different people are working on different stories that affect the same SUT class. The Testcase Class per User Story pattern may or may not end up being the same as Testcase Class per Feature or Testcase Class per Method, depending on how we partition our user stories.

实施说明

Implementation Notes

因为每个测试用例类都代表了 SUT 的单个特性的要求,所以根据测试用例类所验证的特性来命名测试用例类是有意义的。同样,我们可以根据正在验证的 SUT 的测试条件来命名每个测试方法。这种命名法让我们只需查看测试用例类测试方法的名称,就能一目了然地看到所有的测试条件。

Because each Testcase Class represents the requirements for a single feature of the SUT, it makes sense to name the Testcase Class based on the feature it verifies. Similarly, we can name each test method based on which test condition of the SUT is being verified. This nomenclature allows us to see all the test conditions at a glance by merely looking at the names of the Test Methods of the Testcase Class.

使用每个特性的测试用例类的一个后果是,我们最终会为单个生产类提供大量的测试用例类。因为我们仍然想为这个类运行所有测试,所以我们应该将这些测试用例类放入一个嵌套文件夹、包或命名空间中。如果我们使用测试枚举(第399页),我们可以使用AllTests 套件(请参阅第 592页的命名测试套件)将所有测试用例类聚合到一个测试套件中。

One consequence of using Testcase Class per Feature is that we end up with a larger number of Testcase Classes for a single production class. Because we still want to run all the tests for this class, we should put these Testcase Classes into a single nested folder, package, or namespace. We can use an AllTests Suite (see Named Test Suite on page 592) to aggregate all of the Testcase Classes into a single test suite if we are using Test Enumeration (page 399).

激励人心的例子

Motivating Example

此示例使用每个类一个测试用例模式来为具有三个状态(、、和)和四个方法(、、和)的类构建测试方法。因为该类是有状态的,所以我们需要对每个方法的每个状态进行至少一个测试。(为了节省树木,我省略了许多方法主体;请参阅每个类一个测试用例模式以获取完整列表。)FlightUnscheduledScheduledAwaitingApprovalschedulerequestApprovaldeScheduleapprove

This example uses the Testcase Class per Class pattern to structure the Test Methods for a Flight class that has three states (Unscheduled, Scheduled, and AwaitingApproval) and four methods (schedule, requestApproval, deSchedule, and approve. Because the class is stateful, we need at least one test for each state for each method. (In the interest of saving trees, I've omitted many of the method bodies; please refer to Testcase Class per Class for the full listing.)

public class FlightStateTest 扩展了 TestCase {



      public void testRequestApproval_FromScheduledState()

                                抛出异常 {

            Flight flight = FlightTestHelper。

                                      getAnonymousFlightInScheduledState();

            尝试 {

                  flight.requestApproval();

                  失败(“计划状态下不允许”);

          } catch(InvalidRequestException e){

                assertEquals(“InvalidRequestException.getRequest()”,

                                    “requestApproval”,

                                    e.getRequest());

                assertTrue(“isScheduled()”, flight.isScheduled());

        }

    }



    public void testRequestApproval_FromUnsheduledState()

                                抛出异常 {

          Flight flight = FlightTestHelper。

                                     getAnonymousFlightInUnscheduledState();

          flight.requestApproval();

          assertTrue(“isAwaitingApproval()”,

                           flight.isAwaitingApproval());

    }



    public void testRequestApproval_FromAwaitingApprovalState()

                          抛出异常 {

          Flight flight = FlightTestHelper.

                                      getAnonymousFlightInAwaitingApprovalState();

          try {

                flight.requestApproval();

                fail("不允许处于 awaitingApproval 状态");

          } catch (InvalidRequestException e) {

                assertEquals("InvalidRequestException.getRequest()",

                                   "requestApproval",

                                   e.getRequest());

                assertTrue("isAwaitingApproval()",

                                flight.isAwaitingApproval());

        }

    }



    public void testSchedule_FromUnscheduledState()

                               抛出异常 {

          Flight flight = FlightTestHelper.

                             getAnonymousFlightInUnscheduledState();

          flight.schedule();

          assertTrue( "isScheduled()", flight.isScheduled());

    }



    public void testSchedule_FromScheduledState()

                                throws Exception {

      // 我省略了其余测试的主体以

      节省一些树

    }

}

public  class  FlightStateTest  extends  TestCase  {



      public  void  testRequestApproval_FromScheduledState()

                                throws  Exception  {

            Flight  flight  =  FlightTestHelper.

                                      getAnonymousFlightInScheduledState();

            try  {

                  flight.requestApproval();

                  fail("not  allowed  in  scheduled  state");

          }  catch  (InvalidRequestException  e)  {

                assertEquals("InvalidRequestException.getRequest()",

                                    "requestApproval",

                                    e.getRequest());

                assertTrue("isScheduled()",  flight.isScheduled());

        }

    }



    public  void  testRequestApproval_FromUnsheduledState()

                                throws  Exception  {

          Flight  flight  =  FlightTestHelper.

                                     getAnonymousFlightInUnscheduledState();

          flight.requestApproval();

          assertTrue("isAwaitingApproval()",

                           flight.isAwaitingApproval());

    }



    public  void  testRequestApproval_FromAwaitingApprovalState()

                          throws  Exception  {

          Flight  flight  =  FlightTestHelper.

                                      getAnonymousFlightInAwaitingApprovalState();

          try  {

                flight.requestApproval();

                fail("not  allowed  in  awaitingApproval  state");

          }  catch  (InvalidRequestException  e)  {

                assertEquals("InvalidRequestException.getRequest()",

                                   "requestApproval",

                                   e.getRequest());

                assertTrue("isAwaitingApproval()",

                                flight.isAwaitingApproval());

        }

    }



    public  void  testSchedule_FromUnscheduledState()

                               throws  Exception  {

          Flight  flight  =  FlightTestHelper.

                             getAnonymousFlightInUnscheduledState();

          flight.schedule();

          assertTrue(  "isScheduled()",  flight.isScheduled());

    }



    public  void  testSchedule_FromScheduledState()

                                throws  Exception  {

      //  I've  omitted  the  bodies  of  the  rest  of  the  tests  to

      //  save  a  few  trees

    }

}

 

此示例使用Fresh Fixture第 311页)的委托设置第 411页)来实现更具声明性的 Fixture 构造样式。即便如此,此类也变得相当庞大,跟踪测试方法变得有点繁琐。由于此测试用例类上的测试方法需要四种不同的方法,因此它是一个很好的测试示例,可以通过重构为每个特性的测试用例类来改进。

This example uses Delegated Setup (page 411) of a Fresh Fixture (page 311) to achieve a more declarative style of fixture construction. Even so, this class is getting rather large and keeping track of the Test Methods is becoming a bit of a chore. Because the Test Methods on this Testcase Class require four distinct methods, it is a good example of a test that can be improved through refactoring to Testcase Class per Feature.

重构说明

Refactoring Notes

我们可以减小每个测试用例类的大小,并使测试方法的名称更有意义,方法是将它们转换为遵循每个功能测试用例类模式。首先,我们确定要创建多少个类以及每个类中应包含哪些测试方法。如果某些测试用例类最终会比其他类小,那么如果我们从构建较小的类开始,工作就会更容易。接下来,我们执行提取类 [Fowler] 重构以创建一个新的测试用例类,并为其指定一个描述其所执行功能的名称。然后,我们对属于这个新类的每个测试方法及其使用的任何实例变量执行移动方法 [Fowler] 重构(或简单的“剪切和粘贴”)。

We can reduce the size of each Testcase Class and make the names of the Test Methods more meaningful by converting them to follow the Testcase Class per Feature pattern. First, we determine how many classes we want to create and which Test Methods should go into each one. If some Testcase Classes will end up being smaller than others, it makes the job easier if we start by building the smaller classes. Next, we do an Extract Class [Fowler] refactoring to create one of the new Testcase Classes and give it a name that describes the feature it exercises. Then, we do a Move Method [Fowler] refactoring (or a simple "cut and paste") on each Test Method that belongs in this new class along with any instance variables it uses.

我们重复这个过程,直到原始测试用例类中只剩下一个特性;然后我们根据它所执行的特性重命名该类。此时,每个测试用例类都应该编译并运行——但我们还没有完全完成。为了充分利用每个特性测试用例类模式,我们还有最后一步要执行。我们应该对每个测试方法进行重命名方法 [Fowler] 重构,以更好地反映测试方法正在验证的内容。作为重构的一部分,我们可以从每个测试方法名称中删除对正在执行的特性的任何提及——该信息应该包含在测试用例类的名称中。这给我们留下了“空间”,可以在方法名称中包含起始状态(夹具)和预期结果。如果我们对每个特性都有多个具有不同方法参数的测试,我们需要找到一种方法将测试条件的这些方面也包含在方法名称中。

We repeat this process until we are down to just one feature in the original Testcase Class; we then rename that class based on the feature it exercises. At this point, each of the Testcase Classes should compile and run—but we still aren't completely done. To get the full benefit of the Testcase Class per Feature pattern, we have one final step to carry out. We should do a Rename Method [Fowler] refactoring on each of the Test Methods to better reflect what the Test Method is verifying. As part of this refactoring, we can remove any mention of the feature being exercised from each Test Method name—that information should be captured in the name of the Testcase Class. This leaves us with "room" to include both the starting state (the fixture) and the expected result in the method name. If we have multiple tests for each feature with different method arguments, we'll need to find a way to include those aspects of the test conditions in the method name, too.

执行此重构的另一种方法是简单地复制原始测试用例类并按上述方法重命名它们。然后,我们只需删除与每个类不相关的测试方法。我们确实需要小心,不要删除测试方法的所有副本;一个不太严重的疏忽是在几个测试用例类中留下相同方法的副本。我们可以通过为每个功能复制一份原始测试用例类并按上述方法重命名它们来避免这两种潜在错误。然后,我们只需删除与每个类不相关的测试方法。完成后,我们只需删除原始测试用例类。

Another way to perform this refactoring is simply to make copies of the original Testcase Class and rename them as described above. Then we simply delete the Test Methods that aren't relevant for each class. We do need to be careful that we don't delete all copies of a Test Method; a less critical oversight is to leave a copy of the same method in several Testcase Classes. We can avoid both of the potential errors by making one copy of the original Testcase Class for each of the features and rename them as described above. Then we simply delete the Test Methods that aren't relevant for each class. When we are done, we simply delete the original Testcase Class.

示例:每个特性的测试用例类

Example: Testcase Class per Feature

在这个例子中,我们将前面提到的测试集转换为使用每个功能的测试用例类

In this example, we have converted the previously mentioned set of tests to use Testcase Class per Feature.

public class TestScheduleFlight 扩展了 TestCase {



      public void testUnscheduled_shouldEndUpInScheduled()

                     抛出异常 {

        Flight flight = FlightTestHelper。

                                    getAnonymousFlightInUnscheduledState();

        flight.schedule();

        assertTrue( "isScheduled()", flight.isScheduled());

      }



      public void testScheduledState_shouldThrowInvalidRequestEx()

                     抛出异常 {

            Flight flight = FlightTestHelper。

                                      getAnonymousFlightInScheduledState();

            尝试 {

                  flight.schedule();

                  fail("不允许处于预定状态");

            } catch (InvalidRequestException e) {

                  assertEquals("InvalidRequestException.getRequest()",

                                       "schedule",

                                       e.getRequest());

                  assertTrue( "isScheduled()", flight.isScheduled());

            }

      }



      public void testAwaitingApproval_shouldThrowInvalidRequestEx()

                    throws Exception {

            Flight flight = FlightTestHelper.

                                       getAnonymousFlightInAwaitingApprovalState();

            try {

                  flight.schedule();

                  fail("不允许处于预定状态");

            } catch (InvalidRequestException e) {

                  assertEquals("InvalidRequestException.getRequest()",

                                     "schedule",

                                     e.getRequest());

                  assertTrue( "isAwaitingApproval()",

                                     flight.isAwaitingApproval());

            }

      }

}

public  class  TestScheduleFlight  extends  TestCase  {



      public  void  testUnscheduled_shouldEndUpInScheduled()

                     throws  Exception  {

        Flight  flight  =  FlightTestHelper.

                                    getAnonymousFlightInUnscheduledState();

        flight.schedule();

        assertTrue(  "isScheduled()",  flight.isScheduled());

      }



      public  void  testScheduledState_shouldThrowInvalidRequestEx()

                     throws  Exception  {

            Flight  flight  =  FlightTestHelper.

                                      getAnonymousFlightInScheduledState();

            try  {

                  flight.schedule();

                  fail("not  allowed  in  scheduled  state");

            }  catch  (InvalidRequestException  e)  {

                  assertEquals("InvalidRequestException.getRequest()",

                                       "schedule",

                                       e.getRequest());

                  assertTrue(    "isScheduled()",  flight.isScheduled());

            }

      }



      public  void  testAwaitingApproval_shouldThrowInvalidRequestEx()

                    throws  Exception  {

            Flight  flight  =  FlightTestHelper.

                                       getAnonymousFlightInAwaitingApprovalState();

            try  {

                  flight.schedule();

                  fail("not  allowed  in  scheduled  state");

            }  catch  (InvalidRequestException  e)  {

                  assertEquals("InvalidRequestException.getRequest()",

                                     "schedule",

                                     e.getRequest());

                  assertTrue(    "isAwaitingApproval()",

                                     flight.isAwaitingApproval());

            }

      }

}

 

除了名称之外,测试方法实际上并没有改变。因为名称包括先决条件(装置)、正在执行的功能和预期结果,所以当我们在 IDE 的“大纲视图”中查看测试列表时,它们可以帮助我们了解整体情况(参见图 24.2 )。这满足了我们对测试作为文档的需求(参见第23页)。

Except for their names, the Test Methods really haven't changed here. Because the names include the pre-conditions (fixture), the feature being exercised, and the expected outcome, they help us see the big picture when we look at the list of tests in our IDE's "outline view" (see Figure 24.2). This satisfies our need for Tests as Documentation (see page 23).

图 24.2。Eclipse IDE 的 Package Explorer 中显示的“每个功能一个测试用例类”示例。请注意,我们不需要在测试方法名称中包含起始状态,而是为调用的方法名称和预期的结束状态留出空间。

Figure 24.2. Testcase Class per Feature example as seen in the Package Explorer of the Eclipse IDE. Note how we do not need to include the starting state in the Test Method names, leaving room for the name of the method being called and the expected end state.

图像

每个装置的测试用例类

Testcase Class per Fixture

我们如何将测试方法组织到测试用例类中?

How do we organize our Test Methods onto Testcase Classes?

我们根据测试装置的通用性将测试方法组织成测试用例类。

We organize Test Methods into Testcase Classes based on commonality of the test fixture.

图像

随着测试方法(第 348页)数量的增长,我们需要决定将每个测试方法放在哪个测试用例类(第 373页) 中。我们对测试组织策略的选择会影响我们获得测试“全局”视图的难易程度。它还会影响我们对夹具设置策略的选择。

As the number of Test Methods (page 348) grows, we need to decide on which Testcase Class (page 373) to put each Test Method. Our choice of a test organization strategy affects how easily we can get a "big picture" view of our tests. It also affects our choice of a fixture setup strategy.

每个 Fixture使用一个测试用例类让我们可以利用测试自动化框架(第298页)提供的隐式设置(第 424页) 机制。

Using a Testcase Class per Fixture lets us take advantage of the Implicit Setup (page 424) mechanism provided by the Test Automation Framework (page 298).

工作原理

How It Works

我们根据测试方法所需的测试装置作为起点,将测试方法分组到测试用例类中。这种组织方式允许我们使用隐式设置将整个装置设置逻辑移到setUp方法中,从而使每个测试方法专注于四阶段测试(第 358页) 的练习 SUT 和验证结果阶段。

We group our Test Methods onto Testcase Classes based on which test fixture they require as a starting point. This organization allows us to use Implicit Setup to move the entire fixture setup logic into the setUp method, thereby allowing each test method to focus on the exercise SUT and verify outcome phases of the Four-Phase Test (page 358).

何时使用它

When to Use It

当我们有一组需要相同夹具的测试方法,并且我们希望使每个测试方法尽可能简单时,我们可以使用每个夹具一个测试用例类模式。如果每个测试都需要一个唯一的夹具,那么使用每个夹具一个测试用例类就没有什么意义了,因为我们最终会得到大量的单测试类;在这种情况下,最好使用每个功能一个测试用例类第 624页)或简单地使用每个类一个测试用例类第 617页)。

We can use the Testcase Class per Fixture pattern whenever we have a group of Test Methods that need an identical fixture and we want to make each test method as simple as possible. If each test needs a unique fixture, using Testcase Class per Fixture doesn't make a lot of sense because we will end up with a large number of single-test classes; in such a case, it would be better to use either Testcase Class per Feature (page 624) or simply Testcase Class per Class (page 617).

每个装置一个测试用例类的一个好处是,我们可以轻松查看是否正在测试每个起始状态的所有操作。我们最终应该在每个测试用例类上得到相同的测试方法阵容,这在 IDE 的“大纲视图”或“方法浏览器”中非常容易看到。此属性使每个装置一个测试用例类模式特别适用于在投入生产之前发现缺失的单元测试(请参阅第268页的生产错误)。

One benefit of Testcase Class per Fixture is that we can easily see whether we are testing all the operations from each starting state. We should end up with the same lineup of test methods on each Testcase Class, which is very easy to see in an "outline view" or "method browser" of an IDE. This attribute makes the Testcase Class per Fixture pattern particularly useful for discovering Missing Unit Tests (see Production Bugs on page 268) long before we go into production.

每个装置一个测试用例类是行为驱动的测试/规范开发风格的关键部分。它导致非常简短的测试方法,通常每个测试方法只有一个断言。当与总结测试预期结果的测试方法命名约定相结合时,此模式导致测试即文档(参见第23)。

Testcase Class per Fixture is a key part of the behavior-driven development style of testing/specification. It leads to very short test methods, often featuring only a single assertion per test method. When combined with a test method naming convention that summarizes the expected outcome of the test, this pattern leads to Tests as Documentation (see page 23).

实施说明

Implementation Notes

因为我们在测试自动化框架调用的方法(方法)中设置了 Fixture setUp,所以我们必须使用实例变量来保存对我们创建的 Fixture 的引用。在这种情况下,我们必须小心不要使用类变量,因为它可能导致共享 Fixture (第317页) 和经常伴随此类 Fixture 的不稳定测试(第228页)。[第 384页的侧栏“总是有异常”列出了当我们使用实例变量时不保证独立测试(参见第42页) 的 xUnit 成员。]

Because we set up the fixture in a method called by the Test Automation Framework (the setUp method), we must use an instance variable to hold a reference to the fixture we created. In such a case, we must be careful not to use a class variable, as it can lead to a Shared Fixture (page 317) and the Erratic Tests (page 228) that often accompany this kind of fixture. [The sidebar "There's Always an Exception" on page 384 lists xUnit members that don't guarantee Independent Tests (see page 42) when we use instance variables.]

因为每个测试用例类都代表一个测试装置配置,所以根据它创建的装置来命名测试用例类是有意义的。同样,我们可以根据正在执行的 SUT 的方法、传递给 SUT 方法的任何参数的特征以及该方法调用的预期结果来命名每个测试方法。

Because each Testcase Class represents a single test fixture configuration, it makes sense to name the Testcase Class based on the fixture it creates. Similarly, we can name each test method based on the method of the SUT being exercised, the characteristics of any arguments passed to the SUT method, and the expected outcome of that method call.

使用每个装置一个测试用例类的一个副作用是我们最终会得到大量的测试用例类。我们可能需要找到一种方法来对验证单个 SUT 类的各种测试用例类进行分组。一种方法是创建一个嵌套文件夹、包或命名空间来保存这些测试类。如果我们使用测试枚举第 399页),我们还需要创建一个AllTests 套件(请参阅第592页的命名测试套件),以将每个装置的所有测试用例类聚合到一个套件中。

One side effect of using Testcase Class per Fixture is that we end up with a larger number of Testcase Classes. We may want to find a way to group the various Testcase Classes that verify a single SUT class. One way to do so is to create a nested folder, package, or namespace to hold just these test classes. If we are using Test Enumeration (page 399), we'll also want to create an AllTests Suite (see Named Test Suite on page 592) to aggregate all the Testcase Class per Fixtures into a single suite.

另一个副作用是,针对 SUT 的单个特性的测试分散在多个测试用例类中。如果特性彼此密切相关,这种分布可能是一件好事,因为它突出了它们的相互依赖性。相反,如果特性之间没有太大关系,它们的分散可能会令人不安。在这种情况下,我们可以重构为使用每个特性的测试用例类,或者如果我们认为这种症状表明该类承担了太多职责,我们可以对 SUT 应用提取类 [Fowler] 重构。

Another side effect is that the tests for a single feature of the SUT are spread across several Testcase Classes. This distribution may be a good thing if the features are closely related to one another because it highlights their interdependency. Conversely, if the features are somewhat unrelated, their dispersal may be disconcerting. In such a case, we can either refactor to use Testcase Class per Feature or apply an Extract Class [Fowler] refactoring on the SUT if we decide that this symptom indicates that the class has too many responsibilities.

激励人心的例子

Motivating Example

以下示例使用每个类一个测试用例来为具有三个状态(、、和)和四个方法(、、和)的类构建测试方法。由于该类是有状态的,因此我们需要对每个方法的每个状态进行至少一个测试。(为了节省树木,我省略了许多方法主体;请参阅每个类一个测试用例以获取完整列表。)FlightUnscheduledScheduledAwaitingApprovalschedulerequestApprovaldeScheduleapprove)

The following example uses Testcase Class per Class to structure the Test Methods for a Flight class that has three states (Unscheduled, Scheduled, and AwaitingApproval) and four methods (schedule, requestApproval, deSchedule, and approve). Because the class is stateful, we need at least one test for each state for each method. (In the interest of saving trees, I've omitted many of the method bodies; please refer to Testcase Class per Class for the full listing.)

public class FlightStateTest 扩展了 TestCase {



      public void testRequestApproval_FromScheduledState()

                                 抛出异常 {

            Flight flight = FlightTestHelper。

                                      getAnonymousFlightInScheduledState();

            尝试 {

                  flight.requestApproval();

                  失败(“计划状态下不允许”);

            } catch(InvalidRequestException e){

                  assertEquals(“InvalidRequestException.getRequest()”,

                                     “requestApproval”,

                                     e.getRequest());

                  assertTrue(“isScheduled()”, flight.isScheduled());

            }

      }



      public void testRequestApproval_FromUnsheduledState()

                                 抛出异常 {

            Flight flight = FlightTestHelper。

                                      getAnonymousFlightInUnscheduledState();

            flight.requestApproval();

            assertTrue(“isAwaitingApproval()”,

                            flight.isAwaitingApproval());

      }



      public void testRequestApproval_FromAwaitingApprovalState()

                            抛出异常 {

            Flight flight = FlightTestHelper.

                                      getAnonymousFlightInAwaitingApprovalState();

            try {

                  flight.requestApproval();

                  fail("不允许处于 awaitingApproval 状态");

            } catch (InvalidRequestException e) {

                  assertEquals("InvalidRequestException.getRequest()",

                                      "requestApproval",

                                      e.getRequest());

                    assertTrue("isAwaitingApproval()",

                                     flight.isAwaitingApproval());

          }

      }



      public void testSchedule_FromUnscheduledState()

                                抛出异常 {

            Flight flight = FlightTestHelper.

                                      getAnonymousFlightInUnscheduledState();

            flight.schedule();

            assertTrue( "isScheduled()", flight.isScheduled());

      }



      public void testSchedule_FromScheduledState()

                                throws Exception {

        // 我省略了其余测试的主体,以便

        // 拯救一些树木

      }

}

public  class  FlightStateTest  extends  TestCase  {



      public  void  testRequestApproval_FromScheduledState()

                                 throws  Exception  {

            Flight  flight  =  FlightTestHelper.

                                      getAnonymousFlightInScheduledState();

            try  {

                  flight.requestApproval();

                  fail("not  allowed  in  scheduled  state");

            }  catch  (InvalidRequestException  e)  {

                  assertEquals("InvalidRequestException.getRequest()",

                                     "requestApproval",

                                     e.getRequest());

                  assertTrue("isScheduled()",  flight.isScheduled());

            }

      }



      public  void  testRequestApproval_FromUnsheduledState()

                                 throws  Exception  {

            Flight  flight  =  FlightTestHelper.

                                      getAnonymousFlightInUnscheduledState();

            flight.requestApproval();

            assertTrue("isAwaitingApproval()",

                            flight.isAwaitingApproval());

      }



      public  void  testRequestApproval_FromAwaitingApprovalState()

                            throws  Exception  {

            Flight  flight  =  FlightTestHelper.

                                      getAnonymousFlightInAwaitingApprovalState();

            try  {

                  flight.requestApproval();

                  fail("not  allowed  in  awaitingApproval  state");

            }  catch  (InvalidRequestException  e)  {

                  assertEquals("InvalidRequestException.getRequest()",

                                      "requestApproval",

                                      e.getRequest());

                    assertTrue("isAwaitingApproval()",

                                     flight.isAwaitingApproval());

          }

      }



      public  void  testSchedule_FromUnscheduledState()

                                throws  Exception  {

            Flight  flight  =  FlightTestHelper.

                                      getAnonymousFlightInUnscheduledState();

            flight.schedule();

            assertTrue(  "isScheduled()",  flight.isScheduled());

      }



      public  void  testSchedule_FromScheduledState()

                                throws  Exception  {

        //  I've  omitted  the  bodies  of  the  rest  of  the  tests  to

        //  save  a  few  trees

      }

}

 

此示例使用Fresh Fixture第 311页)的委托设置第 411页)来实现更具声明性的 Fixture 构造样式。即便如此,此类也变得相当庞大,跟踪测试方法变得有点繁琐。由于此测试用例类上的测试方法需要三个不同的测试 Fixture(航班可能处于的每个状态一个),因此它是一个很好的测试示例,可以通过重构为每个 Fixture 的测试用例类来改进。

This example uses Delegated Setup (page 411) of a Fresh Fixture (page 311) to achieve a more declarative style of fixture construction. Even so, this class is getting rather large and keeping track of the Test Methods is becoming a bit of a chore. Because the Test Methods on this Testcase Class require three distinct test fixtures (one for each state the flight can be in), it is a good example of a test that can be improved through refactoring to Testcase Class per Fixture.

重构说明

Refactoring Notes

我们可以在夹具设置中删除测试代码重复第 213页),并通过将测试方法转换为使用每个夹具的测试用例类模式,使测试方法更易于理解。首先,我们确定要创建多少个类以及每个类中应该包含哪些测试方法。如果某些测试用例类最终会比其他类小,那么从较小的类开始可以减少我们的工作量。接下来,我们进行提取类重构以创建一个测试用例类,并为其指定一个描述其所需夹具的名称。然后,我们对属于这个新类的每个测试方法以及它使用的任何实例变量进行移动方法 [Fowler] 重构。

We can remove Test Code Duplication (page 213) in the fixture setup and make the Test Methods easier to understand by converting them to use the Testcase Class per Fixture pattern. First, we determine how many classes we want to create and which Test Methods should go into each one. If some Testcase Classes will end up being smaller than others, it will reduce our work if we start with the smaller ones. Next, we do an Extract Class refactoring to create one of the Testcase Classes and give it a name that describes the fixture it requires. Then, we do a Move Method [Fowler] refactoring on each Test Method that belongs in this new class, along with any instance variables it uses.

我们重复这个过程,直到原始类中只剩下一个 Fixture;然后我们可以根据它创建的Fixture重命名该类。此时,每个测试用例类都应该编译并运行 - 但我们还没有完全完成。要充分利用每个 Fixture 模式的测试用例类,我们还有两个步骤要完成。首先,我们应该将每个测试方法中的任何通用 Fixture 设置逻辑分解到setUp方法中,从而产生一个隐式设置。这种类型的设置之所以成为可能,是因为每个类上的测试方法都有相同的 Fixture 要求。其次,我们应该对每个测试方法进行重命名方法 [Fowler] 重构,以更好地反映测试方法正在验证的内容。我们可以从每个测试方法名称中删除对起始状态的任何提及,因为该信息应该包含在测试用例类的名称中。这种重构为我们提供了“空间”,可以在方法名称中包含操作(被调用的方法加上参数的性质)和预期结果。

We repeat this process until we are down to just one fixture in the original class; we can then rename that class based on the fixture it creates. At this point, each of the Testcase Classes should compile and run—but we still aren't completely done. To get the full benefit of the Testcase Class per Fixture pattern, we have two more steps to complete. First, we should factor out any common fixture setup logic from each of the Test Methods into the setUp method, resulting in an Implicit Setup. This type of setup is made possible because the Test Methods on each class have the same fixture requirements. Second, we should do a Rename Method [Fowler] refactoring on each of the Test Methods to better reflect what the Test Method is verifying. We can remove any mention of the starting state from each Test Method name, because that information should be captured in the name of the Testcase Class. This refactoring leaves us with "room" to include both the action (the method being called plus the nature of the arguments) and the expected result in the method name.

正如每个装置一个测试用例类中所描述的,我们也可以重构这个模式,为每个装置制作一个测试用例类的副本(适当命名),从每个副本中删除不必要的测试方法,最后删除旧的测试用例类

As described in Testcase Class per Fixture, we can also refactor to this pattern by making one copy of the Testcase Class (suitably named) for each fixture, deleting the unnecessary Test Methods from each one, and finally deleting the old Testcase Class.

示例:每个 Fixture 的测试用例类

Example: Testcase Class per Fixture

在此示例中,先前的测试集已转换为使用每个 Fixture模式的测试用例类。(为了节省树,我只显示了其中一个结果测试用例类;其他的看起来非常相似。)

In this example, the earlier set of tests has been converted to use the Testcase Class per Fixture pattern. (In the interest of saving trees, I've shown only one of the resulting Testcase Classes; the others look pretty similar.)

public class TestScheduledFlight 扩展了 TestCase {



     Flight ScheduledFlight;



     protected void setUp() 抛出异常 {

           super.setUp();

           scheduleFlight = createScheduledFlight();

     }



     Flight createScheduledFlight() 抛出 InvalidRequestException{

          Flight newFlight = new Flight();

          newFlight.schedule();

          return newFlight;

     }



     public void testDescchedule_shouldEndUpInUnscheduleState()

                                  抛出异常 {

           ScheduledFlight.deschedule();

           assertTrue("isUnsched", ScheduledFlight.isUnscheduled());

     }



     public void testRequestApproval_shouldThrowInvalidRequestEx(){

           try {

                 ScheduledFlight.requestApproval();

                 fail("不允许处于预定状态");

           } catch (InvalidRequestException e) {

                 assertEquals("InvalidRequestException.getRequest()",

                           "requestApproval", e.getRequest());

                 assertTrue("isScheduled()",

                                schedulingFlight.isScheduled());

           }

     }



     public void testSchedule_shouldThrowInvalidRequestEx() {

           try {

                 schedulingFlight.schedule();

                 fail("不允许处于预定状态");

           } catch (InvalidRequestException e) {

                 assertEquals("InvalidRequestException.getRequest()",

                                "schedule", e.getRequest());

                 assertTrue("isScheduled()",

                                schedulingFlight.isScheduled());

           }

     }



     public void testApprove_shouldThrowInvalidRequestEx()

                   throws Exception {

           try {

                 schedulingFlight.approve("Fred");

                 fail("不允许处于预定状态");

           } catch (InvalidRequestException e) {

                 assertEquals("InvalidRequestException.getRequest()",

                                "approve", e.getRequest());

                 assertTrue("isScheduled()",

                                schedulingFlight.isScheduled());

           }

     }

}

public  class  TestScheduledFlight  extends  TestCase  {



     Flight  scheduledFlight;



     protected  void  setUp()  throws  Exception  {

           super.setUp();

           scheduledFlight  =  createScheduledFlight();

     }



     Flight  createScheduledFlight()  throws  InvalidRequestException{

          Flight  newFlight  =  new  Flight();

          newFlight.schedule();

          return  newFlight;

     }



     public  void  testDeschedule_shouldEndUpInUnscheduleState()

                                  throws  Exception  {

           scheduledFlight.deschedule();

           assertTrue("isUnsched",  scheduledFlight.isUnscheduled());

     }



     public  void  testRequestApproval_shouldThrowInvalidRequestEx(){

           try  {

                 scheduledFlight.requestApproval();

                 fail("not  allowed  in  scheduled  state");

           }  catch  (InvalidRequestException  e)  {

                 assertEquals("InvalidRequestException.getRequest()",

                           "requestApproval",  e.getRequest());

                 assertTrue("isScheduled()",

                                scheduledFlight.isScheduled());

           }

     }



     public  void  testSchedule_shouldThrowInvalidRequestEx()  {

           try  {

                 scheduledFlight.schedule();

                 fail("not  allowed  in  scheduled  state");

           }  catch  (InvalidRequestException  e)  {

                 assertEquals("InvalidRequestException.getRequest()",

                                "schedule",  e.getRequest());

                 assertTrue("isScheduled()",

                                scheduledFlight.isScheduled());

           }

     }



     public  void  testApprove_shouldThrowInvalidRequestEx()

                   throws  Exception  {

           try  {

                 scheduledFlight.approve("Fred");

                 fail("not  allowed  in  scheduled  state");

           }  catch  (InvalidRequestException  e)  {

                 assertEquals("InvalidRequestException.getRequest()",

                                "approve",  e.getRequest());

                 assertTrue("isScheduled()",

                                scheduledFlight.isScheduled());

           }

     }

}

 

请注意,每个测试方法都变得多么简单!因为我们对每个测试方法都使用了意图揭示名称[SBPP] ,所以我们可以使用测试作为文档。通过查看 IDE 的“大纲视图”中的方法列表,我们可以看到起始状态(夹具)、操作(被调用的方法)和预期结果(它返回的内容或测试后状态)——所有这些甚至都无需打开方法主体(图 24.3)。

Note how much simpler each Test Method has become! Because we have used Intent-Revealing Names [SBPP] for each of the Test Methods, we can use the Tests as Documentation. By looking at the list of methods in the "outline view" of our IDE, we can see the starting state (fixture), the action (method being called), and the expected outcome (what it returns or the post-test state)—all without even opening up the method body (Figure 24.3).

图 24.3。Eclipse IDE 的 Package Explorer 中显示的每个 Fixture 的 Testcase Class 的测试。请注意,我们不需要在测试方法名称中包含被调用方法的名称,而是为起始状态和预期结束状态留出空间。

Figure 24.3. The tests for our Testcase Class per Fixture as seen in the Package Explorer of the Eclipse IDE. Note how we do not need to include the name of the method being called in the Test Method names, leaving room for the starting state and the expected end state.

图像

我们测试的这种“大局观”视图清楚地表明,我们只在处于 状态approve时测试方法参数。现在我们可以确定这种限制是测试的缺陷还是规范的一部分(即,对于 的某些状态,调用结果为“未定义” )。FlightawaitingApprovalapproveFlight

This "big picture" view of our tests makes it clear that we are only testing the approve method arguments when the Flight is in the awaitingApproval state. We can now decide whether that limitation is a shortcoming of the tests or part of the specification (i.e., the result of calling approve is "undefined" for some states of the Flight).

测试用例超类

Testcase Superclass

也称为

Also known as

抽象测试用例、抽象测试夹具、测试用例基类

Abstract Testcase, Abstract Test Fixture, Testcase Baseclass

当我们的测试代码处于可重复使用的测试实用程序方法中时,我们将其放在哪里?

Where do we put our test code when it is in reusable Test Utility Methods?

我们从抽象的 Testcase Super 类继承了可重用的测试特定逻辑。

We inherit reusable test-specific logic from an abstract Testcase Super class.

图像

在编写测试时,我们总会发现自己需要在许多测试中重复相同的逻辑。最初,我们可能只是在编写需要相同逻辑的其他测试时“克隆和调整”。最终,我们可能会引入测试实用方法第 599页)来保存此逻辑 - 但我们将测试实用方法放在哪里?

As we write tests, we will invariably find ourselves needing to repeat the same logic in many, many tests. Initially, we may just "clone and twiddle" as we write additional tests that need the same logic. Ultimately, we may introduce Test Utility Methods (page 599) to hold this logic—but where do we put the Test Utility Methods?

测试用例超类是我们测试实用方法的一个选择。

A Testcase Superclass is one option as a home for our Test Utility Methods.

工作原理

How It Works

我们定义一个抽象超类来保存可重用的测试实用方法,该方法应该可供多个测试用例类使用(第 373页)。我们使将要重用的方法对子类可见(例如,protected在 Java 中)。然后,我们将此抽象类用作希望重用逻辑的任何测试的超类(基类)。只需调用该方法即可访问逻辑,就像它是在测试用例类本身上定义的一样。

We define an abstract superclass to hold the reusable Test Utility Method that should be available to several Testcase Classes (page 373). We make the methods that will be reused visible to subclasses (e.g., protected in Java). We then use this abstract class as the superclass (base class) for any tests that wish to reuse the logic. The logic can be accessed simply by calling the method as though it were defined on the Testcase Class itself.

何时使用它

When to Use It

如果我们希望在多个测试用例类之间重用测试实用方法,我们可以使用测试用例超类,并且可以找到或定义一个测试用例超类,从中我们可以将所有需要逻辑的测试子类化。

We can use a Testcase Superclass if we wish to reuse Test Utility Methods between several Testcase Classes and can find or define a Testcase Superclass from which we can subclass all tests that require the logic.

该模式假设我们的编程语言支持继承,我们尚未将继承用于其他一些冲突的目的,并且测试实用程序方法不需要访问测试用例超类中不可见的特定类型。

This pattern assumes that our programming language supports inheritance, we are not already using inheritance for some other conflicting purpose, and the Test Utility Method doesn't need access to specific types that are not visible from the Testcase Superclass.

测试用例超类测试助手第 643页)之间的决定取决于类型可见性。客户端类需要查看测试实用程序方法测试实用程序方法需要查看它所依赖的类型和类。当它不依赖于许多类型/类,或者它所依赖的所有内容都可以从一个地方看到时,我们可以将测试实用程序方法放入我们为项目或公司定义的通用测试用例超类中。如果测试实用程序方法依赖于无法从所有客户端都可以访问的单个位置看到的类型/类,则可能需要将其放在适当测试包或子系统中的测试助手上。

The decision between a Testcase Superclass and a Test Helper (page 643) comes down to type visibility. The client classes need to see the Test Utility Method, and the Test Utility Method needs to see the types and classes it depends on. When it doesn't depend on many types/classes or when everything it depends on is visible from a single place, we can put the Test Utility Method into a common Testcase Superclass we define for our project or company. If the Test Utility Method depends on types/classes that cannot be seen from a single place that all clients can access, it may be necessary to put it on a Test Helper in the appropriate test package or subsystem.

变体:测试助手 Mixin

在支持 mixin 的语言中,Test Helper Mixin为我们提供了两全其美的解决方案。与Test Helper一样,我们可以选择要包含哪些Test Helper Mixin,而不受单一继承层次结构的约束。与Test Helper Object(请参阅Test Helper)一样,我们可以在 mixin 中保存特定于测试的状态,但不必实例化并将该任务委托给单独的对象。与Testcase Superclass一样,我们可以将所有内容作为方法和属性进行访问self

In languages that support mixins, Test Helper Mixins give us the best of both worlds. As with a Test Helper, we can choose which Test Helper Mixins to include without being constrained by a single-inheritance hierarchy. As with a Test Helper Object (see Test Helper), we can hold a test-specific state in the mixin but we don't have to instantiate and delegate that task to a separate object. As with a Testcase Superclass, we can access everything as methods and attributes on self.

实施说明

Implementation Notes

在 xUnit 的变体中,所有测试用例类都必须是测试自动化框架(第 298页)提供的测试用例超类的子类,我们将该类定义为我们的测试用例超类的超类。在使用注释或方法属性来标识测试方法(第348页)的变体中,我们可以将任何我们认为有用的类子类化。

In variants of xUnit that require all Testcase Classes to be subclasses of a Testcase Superclass provided by the Test Automation Framework (page 298), we define that class as the superclass of our Testcase Superclass. In variants that use annotations or method attributes to identify the Test Method (page 348), we can subclass any class that we find useful.

我们可以将Testcase Superclass上的方法实现为类方法或实例方法。对于任何无状态的测试实用方法,使用类方法是完全合理的。如果由于某种原因无法使用类方法,我们可以使用实例方法。无论哪种方式,由于方法是继承的,我们可以访问它们,就像它们是在Testcase Class本身上定义的一样。如果我们的语言支持管理方法的可见性,我们必须确保使方法足够可见(例如,protected在 Java 中)。

We can implement the methods on the Testcase Superclass either as class methods or as instance methods. For any stateless Test Utility Methods, it is perfectly reasonable to use class methods. If it isn't possible to use class methods for some reason, we can work with instance methods. Either way, because the methods are inherited, we can access them as though they were defined on the Testcase Class itself. If our language supports managing the visibility of methods, we must ensure that we make the methods visible enough (e.g., protected in Java).

激励人心的例子

Motivating Example

以下示例显示了测试用例类上的测试实用程序方法

The following example shows a Test Utility Method that is on the Testcase Class:

public class TestRefactoringExample extends TestCase {



    public void testAddOneLineItem_quantity1() {

          Invoice inv = createAnonInvoice();

          LineItem expItem = new LineItem(inv, product, QUANTITY);

          // 练习

          inv.addItemQuantity(product, QUANTITY);

          // 验证

          assertInvoiceContainsOnlyThisLineItem(inv, expItem);

    }



    void assertInvoiceContainsOnlyThisLineItem(

                                                           Invoice inv,

                                                           LineItem expItem) {

         List lineItems = inv.getLineItems();

         assertEquals("number of items", lineItems.size(), 1);

         LineItem actual = (LineItem)lineItems.get(0);

         assertLineItemsEqual("",expItem, actual);

    }

}

public  class  TestRefactoringExample  extends  TestCase  {



    public  void  testAddOneLineItem_quantity1()  {

          Invoice  inv  =  createAnonInvoice();

          LineItem  expItem  =  new  LineItem(inv,  product,  QUANTITY);

          //  Exercise

          inv.addItemQuantity(product,  QUANTITY);

          //  Verify

          assertInvoiceContainsOnlyThisLineItem(inv,  expItem);

    }



    void  assertInvoiceContainsOnlyThisLineItem(

                                                           Invoice  inv,

                                                           LineItem  expItem)  {

         List  lineItems  =  inv.getLineItems();

         assertEquals("number  of  items",  lineItems.size(),  1);

         LineItem  actual  =  (LineItem)lineItems.get(0);

         assertLineItemsEqual("",expItem,  actual);

    }

}

 

测试实用程序方法不可在此特定类或其子类之外重复使用。

This Test Utility Method is not reusable outside this particular class or its subclasses.

重构说明

Refactoring Notes

通过使用 Pull Up Method [Fowler] 重构,我们可以将测试实用程序方法移至测试用例超类,从而使其更具可重用性。由于该方法由我们的测试用例类继承,因此我们可以像在本地定义方法一样访问它。如果测试实用程序方法访问任何实例变量,则必须执行 Pull Up Field [Fowler] 重构以将这些变量移至测试实用程序方法可以查看它们的位置。在具有可见性限制的语言中,如果测试用例类上的测试方法也需要访问字段,我们可能需要使字段对子类可见(例如,defaultprotected在 Java 中) 。

We can make the Test Utility Method more reusable by moving it to a Testcase Superclass by using a Pull Up Method [Fowler] refactoring. Because the method is inherited by our Testcase Class, we can access it as if the method were defined locally. If the Test Utility Method accesses any instance variables, we must perform a Pull Up Field [Fowler] refactoring to move those variables to a place where the Test Utility Method can see them. In languages that have visibility restrictions, we may need to make the fields visible to subclasses (e.g., default or protected in Java) if Test Methods on the Testcase Class need to access the fields as well.

示例:测试用例超类

Example: Testcase Superclass

因为该方法被我们的Testcase 类继承,所以我们可以像在本地定义一样访问它。因此用法看起来相同。

Because the method is inherited by our Testcase Class, we can access it as if it were defined locally. Thus the usage looks identical.

public class TestRefactoringExample extends OurTestCase {

      public void testAddItemQuantity_severalQuantity_v12(){

            // Fixture Setup

            Customer cust = createACustomer(new BigDecimal("30"));

            Product prod = createAProduct(new BigDecimal("19.99"));

            Invoice invoice = createInvoice(cust);

            // 练习 SUT

            invoice.addItemQuantity(prod, 5);

            // 验证结果

            LineItem expected = new LineItem(invoice, prod, 5,

                     new BigDecimal("30"), new BigDecimal("69.96"));

            assertContainsExactlyOneLineItem(invoice, expected);

      }

}

public  class  TestRefactoringExample  extends  OurTestCase  {

      public  void  testAddItemQuantity_severalQuantity_v12(){

            //    Fixture  Setup

            Customer  cust  =  createACustomer(new  BigDecimal("30"));

            Product  prod  =  createAProduct(new  BigDecimal("19.99"));

            Invoice  invoice  =  createInvoice(cust);

            //  Exercise  SUT

            invoice.addItemQuantity(prod,  5);

            //  Verify  Outcome

            LineItem  expected  =  new  LineItem(invoice,  prod,  5,

                     new  BigDecimal("30"),  new  BigDecimal("69.96"));

            assertContainsExactlyOneLineItem(invoice,  expected);

      }

}

 

唯一的区别是定义方法的类及其可见性:

The only difference is the class in which the method is defined and its visibility:

公共类 OurTestCase 扩展了 TestCase {

      void assertContainsExactlyOneLineItem(Invoice invoice,

                                                                    LineItem expected) {

           List lineItems = invoice.getLineItems();

           assertEquals("项目数量", lineItems.size(), 1);

           LineItem actItem = (LineItem)lineItems.get(0);

           assertLineItemsEqual("",expected, actItem);

      }

}

public  class  OurTestCase  extends  TestCase  {

      void  assertContainsExactlyOneLineItem(Invoice  invoice,

                                                                    LineItem  expected)  {

           List  lineItems  =  invoice.getLineItems();

           assertEquals("number  of  items",  lineItems.size(),  1);

           LineItem  actItem  =  (LineItem)lineItems.get(0);

           assertLineItemsEqual("",expected,  actItem);

      }

}

 

示例:测试助手 Mixin

Example: Test Helper Mixin

以下是使用 Test::Unit 以 Ruby 编写的一些测试:

Here are some tests written in Ruby using Test::Unit:

def test_extref

     # 设置

     sourceXml = "<extref id='abc'/>"

     expectedHtml = "<a href='abc.html'>abc</a>"

     mockFile = MockFile.new

     @handler = setupHandler(sourceXml, mockFile)

     # 执行

     @handler.printBodyContents

     # 验证

     assert_equals_html( expectedHtml, mockFile.output,

                                    "extref: html 输出")

结束



def testTestterm_normal

      sourceXml = "<testterm id='abc'/>"

      expectedHtml = "<a href='abc.html'>abc</a>"

      mockFile = MockFile.new

      @handler = setupHandler(sourceXml, mockFile)

      @handler.printBodyContents

      assert_equals_html( expectedHtml, mockFile.output,

                                     "testterm: html 输出")

结束



def testTestterm_plural

      sourceXml ="<testterms id='abc'/>"

      expectedHtml = "<a href='abc.html'>abcs</a>"

      mockFile = MockFile.new

      @handler = setupHandler(sourceXml, mockFile)

      @handler.printBodyContents

      assert_equals_html( expectedHtml, mockFile.output,

                                     "testterms: html 输出")

end

def  test_extref

     #  setup

     sourceXml  =  "<extref  id='abc'/>"

     expectedHtml  =  "<a  href='abc.html'>abc</a>"

     mockFile  =  MockFile.new

     @handler  =  setupHandler(sourceXml,  mockFile)

     #  execute

     @handler.printBodyContents

     #  verify

     assert_equals_html(  expectedHtml,  mockFile.output,

                                    "extref:  html  output")

end



def  testTestterm_normal

      sourceXml  =  "<testterm  id='abc'/>"

      expectedHtml  =  "<a  href='abc.html'>abc</a>"

      mockFile  =  MockFile.new

      @handler  =  setupHandler(sourceXml,  mockFile)

      @handler.printBodyContents

      assert_equals_html(  expectedHtml,  mockFile.output,

                                     "testterm:  html  output")

end



def  testTestterm_plural

      sourceXml  ="<testterms  id='abc'/>"

      expectedHtml  =  "<a  href='abc.html'>abcs</a>"

      mockFile  =  MockFile.new

      @handler  =  setupHandler(sourceXml,  mockFile)

      @handler.printBodyContents

      assert_equals_html(  expectedHtml,  mockFile.output,

                                     "testterms:  html  output")

end

 

这些测试包含相当多的测试代码重复第 213页)。我们可以通过使用 Extract Method [Fowler] 重构来创建测试实用程序方法来解决此问题。然后,我们可以通过使用 Pull Up Method 重构将测试实用程序方法移动到测试助手混入,从而使其更具可重用性。由于混入功能被视为测试用例类的一部分,因此我们可以像在本地定义一样访问它。因此用法看起来完全相同。

These tests contain a fair bit of Test Code Duplication (page 213). We can address this issue by using an Extract Method [Fowler] refactoring to create a Test Utility Method. We can then make the Test Utility Method more reusable by moving it to a Test Helper Mixin using a Pull Up Method refactoring. Because the mixed-in functionality is considered part of our Testcase Class, we can access it as if it were defined locally. Thus the usage looks identical.

类 CrossrefHandlerTest < Test::Unit::TestCase

      包括 HandlerTest



      def test_extref

             sourceXml = "<extref id='abc' />"

             expectedHtml = "<a href='abc.html'>abc</a>"

             generateAndVerifyHtml(sourceXml,expectedHtml,"<extref>")

      结束

class  CrossrefHandlerTest    <    Test::Unit::TestCase

      include  HandlerTest



      def  test_extref

             sourceXml  =  "<extref  id='abc'  />"

             expectedHtml  =  "<a  href='abc.html'>abc</a>"

             generateAndVerifyHtml(sourceXml,expectedHtml,"<extref>")

      end

 

唯一的区别是方法定义的位置及其可见性。具体来说,Ruby 要求 mixin 定义在module而不是 中class

The only difference is the location where the method is defined and its visibility. In particular, Ruby requires mixins to be defined in a module rather than a class.

模块 HandlerTest

      def generateAndVerifyHtml( sourceXml, expectedHtml,

                                              message, &block)

            mockFile = MockFile.new

            sourceXml.delete!("\t")

            @handler = setupHandler(sourceXml, mockFile )

            block.call 除非 block == nil

            @handler.printBodyContents

            actual_html = mockFile.output

            assert_equal_html( expectedHtml,

                                           actual_html,

                                           message + "html output")

              actual_html

      end

module  HandlerTest

      def  generateAndVerifyHtml(  sourceXml,  expectedHtml,

                                              message,  &block)

            mockFile  =  MockFile.new

            sourceXml.delete!("\t")

            @handler  =  setupHandler(sourceXml,  mockFile  )

            block.call  unless  block  ==  nil

            @handler.printBodyContents

            actual_html  =  mockFile.output

            assert_equal_html(  expectedHtml,

                                           actual_html,

                                           message  +  "html  output")

              actual_html

      end

 

测试助手

Test Helper

当我们的测试代码处于可重复使用的测试实用程序方法中时,我们将其放在哪里?

Where do we put our test code when it is in reusable Test Utility Methods?

我们定义了一个辅助类来保存我们想要在多个测试中重用的测试实用方法。

We define a helper class to hold any Test Utility Methods we want to reuse in several tests.

图像

在编写测试时,我们总会发现自己需要在许多测试中重复相同的逻辑。最初,我们可能只是在编写需要相同逻辑的其他测试时“克隆和调整”。最终,我们可能会引入测试实用方法第 599页)来保存此逻辑 - 但我们应该把这种可重用的逻辑放在哪里呢?

As we write tests, we will invariably find ourselves needing to repeat the same logic in many, many tests. Initially, we may just "clone and twiddle" as we write additional tests that need the same logic. Ultimately, we may introduce Test Utility Methods (page 599) to hold this logic—but where should we put such reusable logic?

测试助手是可重用测试逻辑的一个可能选择。

A Test Helper is one possible choice of home for reusable test logic.

工作原理

How It Works

我们定义了一个单独的类来保存可重用的测试实用方法,这些方法应该可供多个测试用例类使用(第 373页)。在每个希望使用此逻辑的测试中,我们可以使用静态方法调用或通过专门为此目的创建的实例来访问逻辑。

We define a separate class to hold the reusable Test Utility Methods that should be available to several Testcase Classes (page 373). In each test that wishes to use this logic, we access the logic either using static method calls or via an instance created specifically for the purpose.

何时使用它

When to Use It

如果我们希望在多个测试用例之间共享逻辑或变量,并且无法(或选择不)找到或定义测试用例超类第 638页),否则我们可能会从该超类中子类化所有需要此逻辑的测试,则可以使用测试助手。我们可能会在以下几种情况下采取这种做法:也许我们的编程语言不支持继承(例如,Visual Basic 5 或 6),也许我们已经将继承用于其他一些有冲突的目的,或者也许测试实用程序方法需要访问测试用例超类中不可见的特定类型。

We can use a Test Helper if we wish to share logic or variables between several Testcase Classes and cannot (or choose not to) find or define a Testcase Superclass (page 638) from which we might otherwise subclass all tests that require this logic. We might pursue this course in several circumstances: Perhaps our programming language doesn't support inheritance (e.g., Visual Basic 5 or 6), perhaps we are already using inheritance for some other conflicting purpose, or perhaps the Test Utility Method needs access to specific types that are not visible from the Testcase Superclass.

测试助手测试用例超类之间的选择取决于类型可见性。客户端类需要查看测试实用方法测试实用方法需要查看它所依赖的所有类型和类。当它不依赖于许多类型/类或它所依赖的所有内容都可以从一个地方看到时,我们可以将测试实用方法放入我们为项目或公司定义的通用测试用例超类中。如果测试实用方法所依赖的类型/类无法从所有客户端都可以访问的单个地方看到,则可能需要将其放在适当的测试包或子系统中的测试助手中。在具有许多领域对象组的大型系统中,通常做法是为每组相关领域对象(包)配备一个测试助手。

The decision between a Test Helper and a Testcase Superclass comes down to type visibility. The client classes need to see the Test Utility Method, and the Test Utility Method needs to see all the types and classes it depends on. When it doesn't depend on many types/classes or when everything it depends on is visible from a single place, we can put the Test Utility Method into a common Testcase Superclass we define for our project or company. If the Test Utility Method depends on types/classes that cannot be seen from a single place that all clients can access, it may be necessary to put it on a Test Helper in the appropriate test package or subsystem. In larger systems with many groups of domain objects, it is common practice to have one Test Helper for each group (package) of related domain objects.

变体:测试夹具注册表

注册表[PEAA]是一个众所周知的对象,可以从程序的任何位置访问。我们可以使用注册表来存储和检索程序或测试不同部分的对象。(注册表对象常常与单例[GOF]混淆单例也很众所周知,但是只有一个实例。注册表对象可能有一个或多个实例 - 我们并不关心。)测试装置注册表使测试能够访问与同一测试运行中的其他测试相同的装置。根据我们如何实现测试助手,我们可以选择为每个测试运行器(第 377页) 提供不同的测试装置注册表实例,以防止测试运行战争(请参阅第 228页的异常测试)。测试装置注册表的一个常见示例是数据库沙箱(第 650页)。

A Registry [PEAA] is a well-known object that can be accessed from anywhere in a program. We can use the Registry to store and retrieve objects from different parts of our program or tests. (Registry objects are often confused with Singletons [GOF], which are also well known but have only a single instance. With a Registry object, there may be one or more instances—we don't really care.) A Test Fixture Registry gives the tests the ability to access the same fixture as other tests in the same test run. Depending on how we implement our Test Helper, we may choose to provide a different instance of the Test Fixture Registry for each Test Runner (page 377) in an effort to prevent a Test Run War (see Erratic Test on page 228). A common example of a Test Fixture Registry is the Database Sandbox (page 650).

测试装置注册表通常与设置装饰器(第447页) 或延迟设置(第 435页) 一起使用;套件装置设置(第441页)不需要它,因为只有同一测试用例类上的测试才需要共享装置。在这种情况下,使用装置保存类变量可以很好地实现此目的。

A Test Fixture Registry is typically used with a Setup Decorator (page 447) or with Lazy Setup (page 435); it isn't needed with Suite Fixture Setup (page 441) because only tests on the same Testcase Class need to share the fixture. In such a case, using a fixture holding class variable works well for this purpose.

变体:对象母亲

对象模式只是其他几种模式的简单集合,每种模式都对使测试装置更易于管理做出了虽小但很重要的贡献。对象母由一个或多个测试助手组成,它们提供创建方法(第 415页)和附加方法(请参阅创建方法我们的测试随后会使用这些方法创建可立即使用的测试装置对象。对象母通常提供几种创建同一类的实例的创建方法,其中每种方法都会生成处于不同起始状态的测试对象(命名状态到达方法请参阅创建方法)。对象母还可以自动删除它创建的对象 — 这是自动拆卸(第503页) 的一个例子。

The Object Mother pattern is simply an aggregate of several other patterns, each of which makes a small but significant contribution to making the test fixture easier to manage. The Object Mother consists of one or more Test Helpers that provide Creation Methods (page 415) and Attachment Methods (see Creation Method), which our tests then use to create ready-to-use test fixture objects. Object Mothers often provide several Creation Methods that create instances of the same class, where each method results in a test object in a different starting state (a Named State Reaching Method; see Creation Method). The Object Mother may also have the ability to delete the objects it creates automatically—an example of Automated Teardown (page 503).

因为对于“对象母体”的含义没有单一、明确的定义,所以在提及对象母体的特定功能时建议参考单独的模式(例如自动拆卸) 。

Because there is no single, crisp definition of what someone means by "Object Mother," it is advisable to refer to the individual patterns (such as Automated Teardown) when referring to specific capabilities of the Object Mother.

实施说明

Implementation Notes

测试助手上的方法可以作为类方法或实例方法来实现,这取决于我们想要阻止测试交互的程度。

The methods on the Test Helper can be implemented as either class methods or instance methods depending on the degree to which we want to keep the tests from interacting.

变体:测试助手类

如果所有测试实用方法都是无状态的,最简单的方法是将测试助手的功能实现为类方法,然后让测试使用ClassName.methodName(或等效)符号访问这些方法。如果我们需要保存对夹具对象的引用,我们可以将它们放在类变量中。但是,我们需要小心避免无意中创建共享夹具第 317页)——当然,除非这正是我们想要做的。在这种情况下,我们实际上是在构建一个测试夹具注册表

If all of the Test Utility Methods are stateless, the simplest approach is to implement the functionality of the Test Helper as class methods and then to have the tests access those methods using the ClassName.methodName (or equivalent) notation. If we need to hold references to fixture objects, we could place them in class variables. We need to be careful to avoid inadvertently creating a Shared Fixture (page 317), however—unless, of course, that is exactly what we are trying to do. In such a case, we are actually building a Test Fixture Registry.

变体:测试辅助对象

如果出于某种原因我们不能使用类方法,我们可以改用实例方法。在这种情况下,客户端测试需要创建测试助手类的实例并将其存储在实例变量中;然后可以通过此变量访问方法。当测试助手保存对 Fixture 或 SUT 对象的引用并且我们想确保不会陷入共享 Fixture 的情况时,此模式是一种很好的方法。当测试助手存储一组Mock 对象的期望时(第 544页),它也很有用,因为此模式确保我们可以验证调用是否正确地交错在Mock 对象之间。

If we can't use class methods for some reason, we can work with instance methods instead. In this case, the client test will need to create an instance of the Test Helper class and store it in an instance variable; the methods can then be accessed via this variable. This pattern is a good approach when the Test Helper holds references to fixture or SUT objects and we want to make sure that we don't creep into a Shared Fixture situation. It is also useful when the Test Helper stores expectations for a set of Mock Objects (page 544), because this pattern ensures that we can verify the calls are interleaved between the Mock Objects correctly.

激励人心的例子

Motivating Example

以下示例显示了测试用例类上的测试实用程序方法

The following example shows a Test Utility Method that is on the Testcase Class:

public class TestUtilityExample extends TestCase {



      public void testAddOneLineItem_quantity1() {

           Invoice inv = createAnonInvoice();

           LineItem expItem = new LineItem(inv, product, QUANTITY);

           // 练习

           inv.addItemQuantity(product, QUANTITY);

           // 验证

          assertInvoiceContainsOnlyThisLineItem(inv, expItem);

      }



      void assertInvoiceContainsOnlyThisLineItem(

                                                             Invoice inv,

                                                             LineItem expItem) {

           List lineItems = inv.getLineItems();

           assertEquals("number of items", lineItems.size(), 1);

           LineItem actual = (LineItem)lineItems.get(0);

           assertLineItemsEqual("",expItem, actual);

     }

}

public  class  TestUtilityExample  extends  TestCase  {



      public  void  testAddOneLineItem_quantity1()  {

           Invoice  inv  =  createAnonInvoice();

           LineItem  expItem  =  new  LineItem(inv,  product,  QUANTITY);

           //  Exercise

           inv.addItemQuantity(product,  QUANTITY);

           //  Verify

          assertInvoiceContainsOnlyThisLineItem(inv,  expItem);

      }



      void  assertInvoiceContainsOnlyThisLineItem(

                                                             Invoice  inv,

                                                             LineItem  expItem)  {

           List  lineItems  =  inv.getLineItems();

           assertEquals("number  of  items",  lineItems.size(),  1);

           LineItem  actual  =  (LineItem)lineItems.get(0);

           assertLineItemsEqual("",expItem,  actual);

     }

}

 

测试实用程序方法不可在此特定类之外重复使用。

This Test Utility Method is not reusable outside this particular class.

重构说明

Refactoring Notes

通过将测试实用方法移至测试助手类,我们可以使它更易于重用。这种转换通常与对测试助手类进行移动方法 [Fowler] 重构一样简单。当我们使用实例变量向测试实用方法传递参数或从测试实用方法返回数据时,会出现一个潜在问题执行移动方法重构之前,需要将这些“全局数据”转换为显式参数和返回值。

We can make a Test Utility Method more reusable by moving it to a Test Helper class. This transformation is often as simple as doing a Move Method [Fowler] refactoring to our Test Helper class. One potential problem arises when we have used instance variables to pass arguments to or return data from the Test Utility Method. These "global data" need to be converted to explicit arguments and return values before we can perform the Move Method refactoring.

示例:带有类方法的测试助手

Example: Test Helper with Class Methods

在此修改后的上述测试版本中,我们将测试实用方法转变为测试帮助 上的类方法,以便我们可以通过类名访问它而无需创建实例:

In this modified version of the preceding test, we have turned the Test Utility Method into a class method on a Test Helper Class so we can access it via the classname without creating an instance:

public class TestUtilityExample extends TestCase {

      public void testAddOneLineItem_quantity1_staticHelper() {

            Invoice inv = createAnonInvoice();

            LineItem expItem = new LineItem(inv, product, QUANTITY);

            // 练习

            inv.addItemQuantity(product, QUANTITY);

            // 验证

            TestHelper.assertContainsExactlyOneLineItem(inv, expItem);

      }

}

public  class  TestUtilityExample  extends  TestCase  {

      public  void  testAddOneLineItem_quantity1_staticHelper()  {

            Invoice  inv  =  createAnonInvoice();

            LineItem  expItem  =  new  LineItem(inv,  product,  QUANTITY);

            //  Exercise

            inv.addItemQuantity(product,  QUANTITY);

            //  Verify

            TestHelper.assertContainsExactlyOneLineItem(inv,  expItem);

      }

}

 

示例:带有实例方法的测试助手

Example: Test Helper with Instance Methods

在此示例中,我们将测试实用方法移至测试助手作为实例方法。请注意,我们现在必须通过对象引用(保存测试助手实例的变量)访问该方法。

In this example, we have moved the Test Utility Method to a Test Helper as an instance method. Note that we must now access the method via an object reference (a variable that holds an instance of the Test Helper).

public class TestUtilityExample extends TestCase {

      public void testAddOneLineItem_quantity1_instanceHelper() {

           Invoice inv = createAnonInvoice();

           LineItem expItem = new LineItem(inv, product, QUANTITY);

           // 练习

           inv.addItemQuantity(product, QUANTITY);

           // 验证

           TestHelper helper = new TestHelper();

           helper.assertInvContainsExactlyOneLineItem(inv, expItem);

     }

}

public  class  TestUtilityExample  extends  TestCase  {

      public  void  testAddOneLineItem_quantity1_instanceHelper()  {

           Invoice  inv  =  createAnonInvoice();

           LineItem  expItem  =  new  LineItem(inv,  product,  QUANTITY);

           //  Exercise

           inv.addItemQuantity(product,  QUANTITY);

           //  Verify

           TestHelper  helper  =  new  TestHelper();

           helper.assertInvContainsExactlyOneLineItem(inv,  expItem);

     }

}

 

第 25 章

数据库模式

Chapter 25

Database Patterns

 

本章中的模式

Patterns in This Chapter

数据库沙箱 650

Database Sandbox 650

存储过程测试 654

Stored Procedure Test 654

表截断拆卸 661

Table Truncation Teardown 661

事务回滚拆除 668

Transaction Rollback Teardown 668

数据库沙箱

Database Sandbox

我们如何开发和测试依赖于数据库的软件?

How do we develop and test software that depends on a database?

我们为每个开发人员或测试人员提供单独的测试数据库。

We provide a separate test database for each developer or tester.

图像

许多应用程序使用数据库来存储应用程序的持久状态。此类应用程序的至少部分测试需要访问数据库。不幸的是,由于数据可能在测试之间持续存在,因此数据库是导致测试不稳定(第 228页) 的主要原因。防止测试交互的主要目标是确保每个测试使用的测试装置不重叠。当开发环境仅包含一个测试数据库并且所有开发人员运行的所有测试都针对同一个数据库运行时,这一点尤其困难。

Many applications use a database to store the persistent state of the application. At least some of the tests for such an application will require accessing the database. Unfortunately, a database is a primary cause of Erratic Tests (page 228) due to the fact that data may persist between tests. A major goal in keeping tests from interacting is ensuring that the test fixtures used by each test do not overlap. This is especially difficult when the development environment contains only a single test database and all tests run by all developers run against the same database.

数据库沙箱是一种防止测试通过意外访问数据库中的相同记录而发生交互的方法。

A Database Sandbox is one way to keep the tests from interacting by accidentally accessing the same records in the database.

工作原理

How It Works

我们为每个用户提供一个独立、自洽的沙箱,供其工作。此沙箱包括用户自己的任何代码副本,以及(最重要的是)用户自己的数据库副本。这种安排允许每个用户以自己认为合适的任何方式修改数据库,并通过测试来运行应用程序,而不必担心用户自己的测试与其他用户进行的测试之间的任何交互。

We provide each user with a separate, self-consistent sandbox in which to work. This sandbox includes the user's own copy of any code plus—most importantly—the user's own copy of the database. Such an arrangement allows each user to modify the database in any way he or she sees fit and to exercise the application with tests without worrying about any interactions between the user's own tests and the tests conducted by other users.

何时使用它

When to Use It

每当我们构建或修改依赖于数据库实现其大部分功能的应用程序时,我们都应使用数据库沙箱。如果我们选择使用共享装置第 317页),这种需求就尤其明显。使用数据库沙箱可以帮助我们避免不同数据库用户之间的测试运行战争(请参阅不稳定测试)。根据我们选择实现数据库沙箱的方式,它可能会或可能不会允许不同的用户修改数据库的结构。但是,数据库沙箱不会阻止不可重复的测试(请参阅不稳定的测试)或交互测试(请参阅不稳定的测试),因为它仅将不同的用户(及其测试运行)彼此分隔开来;单次测试运行中的测试可以继续共享测试装置。

We should use a Database Sandbox whenever we are building or modifying an application that depends on a database for a significant portion of its functionality. This need is especially evident if we have chosen to use a Shared Fixture (page 317). Using a Database Sandbox will help us avoid Test Run Wars (see Erratic Test) between different users of the database. Depending on how we have chosen to implement the Database Sandbox, it may or may not allow different users to modify the structure of the database. A Database Sandbox will not prevent Unrepeatable Tests (see Erratic Test) or Interacting Tests (see Erratic Test), however, because it merely separates different users (and their test runs) from one another; tests within a single test run may continue to share a test fixture.

实施说明

Implementation Notes

应用程序需要可配置,以便可以在不修改代码的情况下更改测试中使用的数据库。通常,此目标是通过从每个用户环境中定制的属性文件中读取数据库配置信息来实现的。

The application needs to be made configurable so that the database to be used in testing can be changed without modifying the code. Typically, this goal is accomplished by reading the database configuration information from a properties file that is customized in each user's environment.

数据库沙箱可以通过多种不同的方式实现。从根本上讲,选择取决于我们是为每个用户提供单独的数据库实例还是仅模拟一个数据库实例。一般来说,为每个用户提供真正的单独数据库实例是首选。然而,这种方案可能并不总是可行的——特别是如果数据库供应商的许可结构使其成本过高的话。

A Database Sandbox can be implemented in many different ways. Fundamentally, the choice comes down to whether we give each user a separate database instance or just simulate one. In general, giving each user a real separate database instance is the preferred choice. This scheme may not always be feasible, however—especially if the database vendor's licensing structure makes it cost prohibitive.

变体:专用数据库沙箱

我们为每个开发人员、测试人员或测试用户提供单独的数据库实例。这通常是通过在每个用户的测试环境中安装轻量级数据库技术来实现的。轻量级数据库技术的示例包括 MySql 和 Personal Oracle。数据库实例可以安装在用户自己的机器上、共享测试服务器上或在共享服务器硬件上运行的专用“虚拟服务器”上。

We give each developer, tester, or test user a separate database instance. This is typically accomplished by installing a lightweight database technology in each user's test environment. Examples of lightweight database technologies include MySql and Personal Oracle. The database instance can be installed on the user's own machine, on a shared test server, or on a dedicated "virtual server" running on shared server hardware.

专用数据库沙箱是首选解决方案,因为它提供了最大的灵活性。它允许开发人员修改数据库架构、加载自己的测试数据等。

A Dedicated Database Sandbox is the preferred solution because it provides the greatest flexibility. It allows a developer to modify the database schema, load his or her own test data, and so on.

变体:每个测试运行器的数据库模式

通过每个测试运行器的 DB Schema,我们为每个开发人员、测试人员或测试用户提供看似单独的数据库实例,方法是使用内置的数据库支持多种模式。

With DB Schema per Test Runner, we give each developer, tester, or test user what appears to be a separate database instance by using built-in database support for multiple schemas.

相对于专用数据库沙箱模式,每个测试运行器一个数据库模式的一个显著优势是,我们可以共享一个在公共模式中定义的不可变共享装置(参见共享装置),并将每个用户的可变装置放在他或她自己的私有模式中。请注意,此方案不允许用户修改数据库的结构(至少不能达到专用数据库沙箱所能达到的程度)。它还强制所有用户(包括开发人员和测试人员)使用相同的数据库结构。当需要推出数据库结构升级时,这可能会产生后勤问题。

One considerable advantage that the DB Schema per Test Runner pattern enjoys relative to the Dedicated Database Sandbox pattern is that we can share an Immutable Shared Fixture (see Shared Fixture) defined in a common schema and put each user's mutable fixture in his or her own private schema. Note that this scheme does not allow the user to modify the structure of the database (at least not to the same degree as is possible with a Dedicated Database Sandbox). It also forces all users, including both developers and testers, to use the same database structure. This can create logistical issues when database structure upgrades need to be rolled out.

变体:数据库分区方案

我们为每个开发人员、测试人员或测试用户在单个数据库沙箱中提供一组单独的数据。每个用户都可以根据自己的需要修改该数据,但不允许修改分配给其他用户的数据。

We give each developer, tester, or test user a separate set of data within a single Database Sandbox. Each user can modify that data as he or she sees fit but is not allowed to modify the data assigned to other users.

与实现数据库沙箱的其他方法相比,这种方法需要的数据库管理开销较少,但数据管理开销较多。由于数据库分区方案不允许开发人员修改数据库模式,因此它不适用于进化式数据库开发。然而,当应用于从同一个测试运行器运行的不同测试时,它适用于防止交互测试。也就是说,我们为每个测试赋予一个唯一的键,例如,它用于所有数据。因此,同一测试运行中的其他测试使用不同的数据。当使用共享装置时,这种模式可以与数据库沙箱的许多其他变体结合使用,以防止交互测试。请注意,除非我们还使用不同的生成值(请参阅第 723页的生成值),否则此模式不能防止不可重复的测试。CustomerNumber

This approach requires less database administration overhead but more data administration overhead than with the other ways to implement a Database Sandbox. Because it does not allow developers to modify the database schema, a Database Partitioning Scheme is not appropriate for evolutionary database development. It is, however, appropriate for preventing Interacting Tests when applied to different tests run from the same Test Runner. That is, we give each test a unique key such as a CustomerNumber that it uses for all data. As a consequence, other tests within the same test run use different data. This pattern can be combined with many of the other variations of Database Sandbox to prevent Interacting Tests when using a Shared Fixture. Note that this pattern does not prevent Unrepeatable Tests unless we also use Distinct Generated Values (see Generated Value on page 723).

激励人心的例子

Motivating Example

以下测试使用文字值作为构造函数的参数,Product该构造函数持久保存在多个开发人员共享的数据库实例中。名称Product必须是唯一的:

The following test uses Literal Values for the arguments to a constructor of a Product that is persisted into a database instance shared among several developers. The name of the Product must be unique:

public void testProductPrice_HCV() {

      // 设置

      产品 product =

            new Product( 88, // ID

                                  "Widget", // 名称

                                  new BigDecimal("19.99")); // 价格

      // 练习 SUT

      // ...

}

public  void  testProductPrice_HCV()  {

      //        Setup

      Product  product  =

            new  Product(  88,                                     //  ID

                                  "Widget",                           //  Name

                                  new  BigDecimal("19.99"));  //  Price

      //  Exercise  SUT

      //      ...

}

 

不幸的是,当我们针对共享数据库实例运行此测试时,无论我们在每次测试后如何有效地拆除,都可能最终导致测试运行战争Product。这是因为我们试图创建另一个测试运行器可能正在同时Product使用的相同测试运行。

Unfortunately, we may end up with a Test Run War when we run this test against a shared database instance regardless of how effectively we tear down the Product after each test. This is because we are trying to create the same Product that the same test run from another Test Runner might be in the process of using at the same time.

重构说明

Refactoring Notes

当我们为每个开发人员和测试人员创建专用数据库沙箱时,我们的测试不需要更改代码。因此,测试不需要做任何特殊的事情就可以完全独立于其他测试运行器(第 377页) 运行的测试运行。但是,需要对 SUT 进行一些小的更改,以允许 SUT 根据配置数据连接到不同的数据库实例。我们如何进行这种更改因我们使用的技术而异,这超出了本书的范围。

There are no code changes required of our test when we create a Dedicated Database Sandbox for each developer and tester. Therefore, tests should not have to do anything special to run completely independently of tests being run from other Test Runners (page 377). There is a small change required of the SUT, however, to allow the SUT to connect to different database instances based on configuration data. How we make this change varies with the technology we use and is beyond the scope of this book.

我们可以将测试转换为使用数据库分区方案,通过将文字值替换为对适当方法的调用并将特定于测试运行器的getUniqueID作为种子传递。

We can convert the test to use a Database Partitioning Scheme by replacing the Literal Values with calls to the appropriate getUnique method passing an ID specific to the Test Runner as a seed.

示例:数据库分区方案

Example: Database Partitioning Scheme

这是使用数据库分区方案进行的相同测试,以确保每个测试使用不同的产品集。对于该getUniqueString方法,我们根据计算机的 MAC 地址传递一个字符串。

Here is the same test using a Database Partitioning Scheme to ensure that each test uses a different set of products. For the getUniqueString method, we pass a string based on the MAC address of our computer.

public void testProductPrice_DPS() {

      // 设置

      产品 product =

            new Product( getUniqueInt(), // ID

                                  getUniqueString(getMacAddress()), // 名称

                                  new BigDecimal("19.99")); // 价格

      // 练习 SUT

      // ...

}

static int counter = 0;

int getUniqueInt() {

      counter++;

      return counter;

}

BigDecimal getUniqueBigDecimal() {

      return new BigDecimal(getUniqueInt());

}

String getUniqueString(String baseName) {

      return baseName.concat(String.valueOf( getUniqueInt()));

}

public  void  testProductPrice_DPS()  {

      //  Setup

      Product  product  =

            new  Product(  getUniqueInt(),                                //  ID

                                  getUniqueString(getMacAddress()),  //  Name

                                  new  BigDecimal("19.99"));               //  Price

      //  Exercise  SUT

      //        ...

}

static  int  counter  =  0;

int  getUniqueInt()  {

      counter++;

      return  counter;

}

BigDecimal  getUniqueBigDecimal()  {

      return  new  BigDecimal(getUniqueInt());

}

String  getUniqueString(String  baseName)  {

      return  baseName.concat(String.valueOf(  getUniqueInt()));

}

 

现在可以从多台不同的计算机针对同一个共享数据库实例运行此测试,而不必担心测试运行战

This test can now be run from several different computers against the same shared database instance without fear of a Test Run War.

存储过程测试

Stored Procedure Test

当我们有存储过程时,我们如何独立验证逻辑?

How can we verify logic independently when we have stored procedures?

我们为每个存储过程编写全自动测试。

We write Fully Automated Tests for each stored procedure.

图像

许多使用数据库存储应用程序持久状态的应用程序还使用存储过程和触发器来提高性能并对更新执行常见处理。

Many applications that use a database to store the persistent state of the application also use stored procedures and triggers to improve performance and do common processing on updates.

存储过程测试是一种将自动化测试实践应用于数据库内部代码的方法。

A Stored Procedure Test is a way to apply automated testing practices to this code that lives inside the database.

工作原理

How It Works

我们为独立于客户端应用软件的存储过程编写单元测试。这些测试可能是跨层测试或往返测试,具体取决于被测试存储过程的性质。

We write unit tests for the stored procedures independent of the client application software. These tests may be layer-crossing tests or round-trip tests, depending on the nature of the store procedure(s) being tested.

何时使用它

When to Use It

只要存储过程涉及重要逻辑,我们就应该编写存储过程测试。此模式有助于我们验证存储过程(这些测试中的 SUT)是否独立于客户端应用程序正常运行。当多个应用程序使用存储过程,或存储过程由不同的开发团队开发时,这一点尤其重要。当我们无法仅通过运行应用程序软件(一种间接测试;请参见第186页的模糊测试)来确保过程得到充分测试时,存储过程测试尤其重要。使用存储过程测试还可以帮助我们列举出调用存储过程的所有条件以及每种情况下应该发生的情况。思考这些情况本身就可能改进设计 — 这是测试先行开发的常见结果。

We should write Stored Procedure Tests whenever we have nontrivial logic in stored procedures. This pattern will help us verify that the stored procedures—our SUT for the purposes of these tests—are working properly independently of the client application. This consideration is particularly important when more than one application will use the stored procedures or when the stored procedures are being developed by a different development team. Stored Procedure Tests are particularly important when we cannot ensure the procedures are tested adequately simply by exercising the application software (a form of Indirect Testing; see Obscure Test on page 186). Using Stored Procedure Tests also helps us to enumerate all the conditions under which the stored procedure could be called and what should happen in each circumstance. The very act of thinking about these circumstances is likely to improve the design—a common result of doing test-first development.

实施说明

Implementation Notes

有两种完全不同的方法来实现存储过程测试: (1) 我们可以用与存储过程相同的编程语言编写测试,并在数据库中运行它们;或者 (2) 我们可以用我们的应用程序编程语言编写测试,并通过远程代理[GOF]访问存储过程。我们甚至可以双向编写测试。例如,存储过程开发人员可以用数据库编程语言编写单元测试,而应用程序开发人员可以用应用程序编程语言准备一些验收测试,作为应用程序构建的一部分运行。

There are two fundamentally different ways to implement Stored Procedure Tests: (1) We can write the tests in the same programming language as the stored procedure and run them in the database or (2) we can write the tests in our application programming language and access the stored procedure via a Remote Proxy [GOF]. We might even write tests both ways. For example, the stored-procedure developers might write unit tests in the database programming language, whereas the application developers might prepare some acceptance tests in the application programming language to run as part of the application build.

无论哪种方式,我们都需要决定测试将如何设置装置(数据库的“之前”状态)并验证预期结果(数据库的“之后”状态以及任何预期操作,如级联删除)。测试可以直接与数据库交互以插入/验证数据(一种后门操作;参见第 327页),或者可以使用另一个存储过程(一种往返测试)。

Either way, we need to decide how the test will set up the fixture (the "before" state of the database) and verify the expected outcome (the "after" state of the database as well as any expected actions such as cascading deletes). The test may interact directly with the database to insert/verify the data (a form of Back Door Manipulation; see page 327) or it could use another stored procedure (a form of round-trip test).

变体:数据库内存储过程测试

xUnit 自动化测试方法的一个优点是测试使用与测试代码相同的语言编写。这使开发人员更容易学习如何自动化测试,而无需学习新的编程语言、调试器等。将这个想法延伸到其逻辑结论,使用以存储过程编程语言编写的测试来测试存储过程是有意义的。当然,我们需要在数据库内运行这些测试。不幸的是,这一要求可能会使它们很难作为集成构建 [SCM] 的一部分运行。

One advantage of the xUnit approach to automated testing is that the tests are written in the same language as the code we are testing. This makes it easier for the developers to learn how to automate the tests without learning a new programming language, debugger, and so on. Extending this idea to its logical conclusion, it makes sense to test stored procedures using tests that are written in the stored-procedure programming language. Naturally, we will need to run these tests inside the database. Unfortunately, that requirement may make it hard to run them as part of the Integration Build [SCM].

当我们在存储过程语言和/或环境中编写代码的经验比在应用程序环境中编写代码的经验丰富时,这种存储过程测试模式的变体是合适的,并且所有测试都不必从单个位置运行。例如,正在编写供其他团队使用的存储过程的数据库或数据服务团队会发现这种方法很有吸引力。另一种适合使用数据库内存储过程测试的情况是,当过程存储在与应用程序逻辑不同的源代码存储库中时。使用数据库内存储过程测试允许我们将测试存储在与 SUT(在本例中为存储过程)相同的存储库中。

This variation on the Stored Procedure Test pattern is appropriate when we have more experience writing code in the stored-procedure language and/or environment than in the application environment and it is not essential that all tests be run from a single place. For example, a database or data services team that is writing stored procedures for use by other teams would find this approach attractive. Another circumstance in which it would be appropriate to use In-Database Stored Procedure Tests arises when the procedures are stored in a different source code repository than the application logic. Using In-Database Stored Procedure Test allows us to store the tests in the same repository as the SUT (in this case, the stored procedures).

数据库内存储过程测试可能允许对存储过程进行更彻底的单元测试(和测试驱动开发),因为我们可以从测试中更好地了解存储过程的实现细节。当然,这种违反封装的行为可能会导致过度指定的软件(请参阅第239页的脆弱测试)。如果客户端代码使用数据访问层,我们仍然必须用应用程序编程语言为该软件编写单元测试,以确保我们正确处理错误(例如,连接失败)。

In-Database Stored Procedure Tests may allow somewhat more thorough unit testing (and test-driven development) of the stored procedures because we may have better access to implementation details of the stored procedure from our tests. Of course, this violation of encapsulation could result in Overspecified Software (see Fragile Test on page 239). If the client code uses a data access layer, we must still write unit tests for that software in the application programming language to ensure that we handle errors correctly (e.g., failure to connect).

一些数据库支持多种编程语言。在这种情况下,我们可以选择使用更适合测试的编程语言进行测试,但使用更传统的存储过程编程语言编写存储过程。例如,Oracle 数据库同时支持 PLSQL 和 Java,因此我们可以使用 JUnit 测试来验证我们的 PLSQL 存储过程。同样,Microsoft 的 SQL Server 支持 C#,因此我们可以使用用 C# 编写的 NUnit 测试来验证用 Transact-SQL 编写的存储过程。

Some databases support several programming languages. In such a case, we can choose to use the more test-friendly programming language for our tests but write the stored procedures in the more traditional stored-procedure programming language. For example, Oracle databases support both PLSQL and Java, so we could use JUnit tests to verify our PLSQL stored procedures. Likewise, Microsoft's SQL Server supports C#, so we could use NUnit tests written in C# to verify the stored procedures written in Transact-SQL.

变体:远程存储过程测试

远程存储过程测试的目的是让我们能够使用与客户端应用程序逻辑的单元测试相同的语言来编写测试。我们必须通过隐藏与该过程交互机制的远程代理[GOF]访问存储过程。此代理可以构造为服务外观[CJ2EEP]命令 [GOF](例如 Java 的JdbcOdbcCallableStatement)。

The purpose of Remoted Stored Procedure Tests is to allow us to write the tests in the same language as the unit tests for the client application logic. We must access the stored procedure via a Remote Proxy [GOF] that hides the mechanics of interacting with that procedure. This proxy can be structured as either a Service Facade [CJ2EEP] or a Command [GOF] (such as Java's JdbcOdbcCallableStatement).

远程存储过程测试实际上是组件测试,因为它们将存储过程视为“黑盒”组件。由于远程存储过程测试不在数据库内运行,因此除非我们有一种简单的方法来插入或验证数据,否则我们更有可能将它们编写为往返测试(调用其他存储过程来设置装置、验证结果并执行其他必要任务)。xUnit 系列的一些成员具有专门用于促进这种行为的扩展(例如,用于 Java 的 DbUnit 和用于 .NET 语言的 NDbUnit)。

Remoted Stored Procedure Tests are, in effect, component tests in that they treat the stored procedure as a "black box" component. Because Remoted Stored Procedure Tests do not run inside the database, we are more likely to write them as round-trip tests (calling other stored procedures to set up the fixture, verify the outcome, and perform other necessary tasks) unless we have an easy way to insert or verify data. Some members of the xUnit family have extensions that are specifically intended to facilitate this behavior (e.g., DbUnit for Java and NDbUnit for .NET languages).

如果我们想将所有测试都保留在一种编程语言中,那么此解决方案更合适。远程存储过程测试模式使我们每次签入应用程序代码的更改时都可以更轻松地运行所有测试。

This solution is more appropriate if we want to keep all our tests in a single programming language. The Remoted Stored Procedure Test pattern makes it easier to run all the tests every time we check in changes to the application code.


使用 JUnit 测试存储过程

在早期的 XP 项目中,我们的应用程序被要求使用另一个团队开发的存储过程。似乎每次我们将 Java 与那些开发人员的 PLSQL 代码集成时,我们都会发现他们的存储过程的基本行为中存在严重错误。我们正在使用 JUnit 为我们的代码编写自动化测试。虽然我们确信为存储过程编写单元测试将澄清接口契约并提高其他团队代码的质量,但我们不能强迫其他团队编写单元测试。当时甚至还没有发明utPLSQL 。

我们决定尝试在我们熟悉的 xUnit 家族成员 JUnit 中为存储过程编写单元测试。因为我们无论如何都必须编写 JDBC 代码来访问存储过程,所以我们通过PreparedStatement我们构建的 JDBC 类为每个存储过程定义了 JUnit 测试。测试执行了存储行为的基本行为和一些更明显的失败案例。每当我们收到新版本的存储过程时,我们都会先运行 JUnit 测试,然后再尝试从应用程序代码中执行过程。不用说,许多测试都失败了。

我们与正在构建存储过程的开发人员坐下来,向他们展示了我们的测试 — 包括它们是如何失败的。不用说,开发人员有点尴尬,但他们同意我们的测试是正确的。他们去修复存储过程,并给了我们一个新版本进行测试。修订版的表现稍好一些,但仍然产生了一些失败。然后发生了一件非常重要的事情:另一组的成员要求提供我们编写的测试副本以及如何自己运行它们的说明。不久之后,这些开发人员就开始在 JUnit 中编写自己的 PLSQL 单元测试了!



Testing Stored Procedures with JUnit

On an early XP project, our application was mandated to use stored procedures being developed by another group. It seemed that every time we integrated our Java with those developers' PLSQL code, we found serious bugs in the fundamental behavior of their stored procedures. We were writing automated tests using JUnit for our code. Although we were sure that writing unit tests for the stored procedures would clarify the interface contract and improve the quality of the other group's code, we couldn't force the other team to write unit tests. Nor had utPLSQL even been invented at that point.

We decided to try writing unit tests for the stored procedures in the xUnit family member we were comfortable with: JUnit. Because we had to write JDBC code to access the stored procedures anyway, we defined JUnit tests for each stored procedure via the JDBC PreparedStatement classes that we had built. The tests exercised the basic behavior of the stored behaviors and a few of the more obvious failure cases. Whenever we received a new version of the stored procedures, we would run the JUnit tests before we even tried to exercise the procedures from our application code. Needless to say, many of the tests failed.

We sat down with the developers who were building the stored procedures and showed them our tests—including how they were failing left, right, and center. Needless to say, the developers were a bit embarrassed but they agreed that our tests were correct. They went off to fix the stored procedures and gave us a new version to test. The revision fared somewhat better but still produced some failures. Then a very important thing happened: The members of the other group asked for a copy of the tests we had written and instructions on how to run them for themselves. Before long, these developers were writing their own PLSQL unit tests in JUnit!


 

如果存储过程是由开发客户端代码的同一团队编写和/或修改的,则此功能特别有用。当另一个团队提供存储过程并且我们对这些开发人员编写无缺陷代码的能力没有信心时(可能是因为他们没有为其代码编写数据库内存储过程测试),我们也可以使用远程存储过程测试。在这种情况下,我们可以使用远程存储过程测试作为其代码的验收测试形式。请参阅侧栏“使用 JUnit 测试存储过程”,了解此设置如何在一个项目上运行。

This capability is particularly useful if the stored procedures are being written and/or modified by the same team that is developing the client code. We can also use Remoted Stored Procedure Tests when another team is providing the stored procedures and we are not confident in those developers' ability to write defect-free code (probably because they are not writing In-Database Stored Procedure Tests for their code). In this situation, we can use the Remoted Stored Procedure Tests as a form of acceptance test for their code. See the sidebar "Testing Stored Procedures with JUnit" for an illustration of how this setup worked on one project.

使用远程存储过程测试的一个缺点是,它们可能会导致测试套件运行速度变慢,因为测试需要数据库可用并填充数据。存储过程的测试可以放入单独的子集套件中(请参阅第 592页的命名测试套件),这样它们就不需要与所有内存测试一起运行。这可以显著加快测试执行速度,从而避免测试速度变慢(第253页)。

One disadvantage of using Remoted Stored Procedure Tests is that they will likely cause the test suite to run more slowly because the tests require the database to be available and populated with data. The tests for the stored procedures can be put into a separate Subset Suite (see Named Test Suite on page 592) so that they need not be run with all the in-memory tests. This can significantly speed up test execution, thereby avoiding Slow Tests (page 253).

当我们选择的编程语言编写的逻辑已经有单元测试,而我们需要将该逻辑移到数据库中时,远程存储过程测试也非常有用。通过使用远程存储过程测试,我们可以避免用不同的编程语言和测试自动化框架(第 298页)重写测试,从而节省时间和金钱。这种模式还使我们能够在重新编码逻辑时避免任何翻译错误,因此我们可以确保重新编码的逻辑确实会产生相同的结果。

Remoted Stored Procedure Tests also come in handy when logic written in our programming language of choice already has unit tests and we need to move that logic into the database. By using a Remoted Stored Procedure Test, we can avoid rewriting the tests in a different programming language and Test Automation Framework (page 298), which can in turn save time and money. This pattern also enables us to avoid any translation errors when recoding the logic, so we can be sure the recoded logic really does produce the same results.

激励人心的例子

Motivating Example

以下是用 PLSQL 编写的存储过程的示例:

Here is an example of a stored procedure written in PLSQL:

创建或替换过程 calc_secs_between (

      date1 IN DATE,

      date2 IN DATE,

      secs OUT NUMBER

)

IS

BEGIN

      secs := (date2 - date1) * 24 * 60 * 60;

END;

/

CREATE  OR  REPLACE  PROCEDURE  calc_secs_between  (

      date1  IN  DATE,

      date2  IN  DATE,

      secs  OUT  NUMBER

)

IS

BEGIN

      secs  :=  (date2  -  date1)  *  24  *  60  *  60;

END;

/

 

此示例取自 utPLSQL 工具附带的示例。在实际生活中,我们可能不会费心测试此代码,因为它非常简单(但话又说回来,也许不会?),但它可以很好地说明我们如何对其进行测试。

This sample was taken from the examples that come with the utPLSQL tool. In real life we might not bother testing this code because it is so simple (but then again, maybe not?) but it will work just fine to illustrate how we could go about testing it.

重构说明

Refactoring Notes

此示例与重构关系不大,而是添加了缺失的测试。让我们想办法编写一个。我们将使用两个主要变体来了解所涉及的内容:数据库内存储过程测试远程存储过程测试

This example doesn't deal so much with refactoring as with adding a missing test. Let's find a way to write one. We will see what is involved by using the two main variants: In-Database Stored Procedure Test and Remote Stored Procedure Test.

示例:数据库内存储过程测试

Example: In-Database Stored Procedure Test

此示例使用 utPLSQL(PLSQL 的 xUnit 系列成员)来自动执行数据库内运行的测试:

This example uses utPLSQL, the xUnit family member for PLSQL, to automate tests that run inside the database:

创建或替换包体 ut_calc_secs_between

IS

      PROCEDURE ut_setup

        IS

        BEGIN

              NULL;

        END;



        PROCEDURE ut_teardown

        IS

        BEGIN

              NULL;

        END;



        -- 对于每个要测试的程序...

        PROCEDURE ut_CALC_SECS_BETWEEN

        IS

              secs PLS_INTEGER;

        BEGIN

              CALC_SECS_BETWEEN (

                          DATE1 => SYSDATE

                          '

                          DATE2 => SYSDATE

                          '

                          SECS => secs

                );



              utAssert.eq (

                    'Same dates',

                    secs,

                    0

                    );

      END ut_CALC_SECS_BETWEEN;



END ut_calc_secs_between;

/

CREATE  OR  REPLACE  PACKAGE  BODY  ut_calc_secs_between

IS

      PROCEDURE  ut_setup

        IS

        BEGIN

              NULL;

        END;



        PROCEDURE  ut_teardown

        IS

        BEGIN

              NULL;

        END;



        --  For  each  program  to  test...

        PROCEDURE  ut_CALC_SECS_BETWEEN

        IS

              secs  PLS_INTEGER;

        BEGIN

              CALC_SECS_BETWEEN  (

                          DATE1  =>  SYSDATE

                          '

                          DATE2  =>  SYSDATE

                          '

                          SECS  =>  secs

                );



              utAssert.eq  (

                    'Same  dates',

                    secs,

                    0

                    );

      END  ut_CALC_SECS_BETWEEN;



END  ut_calc_secs_between;

/

 

此测试使用了许多熟悉的 xUnit 模式。这是我们通常为此存储过程编写的几个测试之一 — 针对每个可能的情况编写一个测试。(此示例取自 utPLSQL 工具附带的示例。我不是 PLSQL 程序员,所以我不想弄乱格式,以防万一!)

This test uses many of the familiar xUnit patterns. It is one of several tests we would normally write for this stored procedure—one test for each possible scenario. (This sample was taken from the examples that come with the utPLSQL tool. Not being a PLSQL programmer, I did not want to mess with the formatting in case it mattered!)

示例:远程存储过程测试

Example: Remoted Stored Procedure Test

为了在正常的编程和测试执行环境中测试此存储过程,我们必须首先在所选的单元测试环境中为其找到或创建一个远程代理。然后,我们就可以按照通常的方式编写单元测试。

To test this stored procedure in our normal programming and test execution environment, we must first find or create a Remote Proxy for it in our unit-testing environment of choice. Then we can write our unit tests in the usual manner.

以下测试使用 JUnit 自动执行在数据库外运行的测试并远程调用我们的 PLSQL 存储过程:

The following test uses JUnit to automate tests that run outside the database and call our PLSQL stored procedure remotely:

public class StoredProcedureTest extends TestCase {

      public void testCalcSecsBetween_SameTime() {

            // 设置

            TimeCalculatorProxy SUT = new TimeCalculatorProxy();

            Calendar cal = new GregorianCalendar();

            long now = cal.getTimeInMillis();

            // 练习

            long timeDifference = SUT.calc_secs_between(now,now);

            // 验证

            assertEquals( 0, timeDifference );

      }

}

public  class  StoredProcedureTest  extends  TestCase  {

      public  void  testCalcSecsBetween_SameTime()  {

            //  Setup

            TimeCalculatorProxy  SUT  =  new  TimeCalculatorProxy();

            Calendar  cal  =  new  GregorianCalendar();

            long  now  =  cal.getTimeInMillis();

            //  Exercise

            long  timeDifference  =  SUT.calc_secs_between(now,now);

            //  Verify

            assertEquals(  0,  timeDifference  );

      }

}

 

我们通过将原始测试隐藏在 Service Facade 后面,将原始测试的复杂性降低为对函数的简单测试JdbcOdbcCallableStatement。查看此示例,很难看出我们不是在测试 Java 方法。我们可能会有额外的预期异常测试(请参阅第348页的测试方法)来验证失败的连接和其他问题。

We have reduced the complexity of the original test to a simple test of a function by hiding the JdbcOdbcCallableStatement behind a Service Facade. Looking at this example, it is difficult to tell that we are not testing a Java method. We would probably have additional Expected Exception Tests (see Test Method on page 348) to verify failed connections and other problems.

表截断拆除

Table Truncation Teardown

当测试装置位于关系数据库中时,我们如何拆除它?

How do we tear down the Test Fixture when it is in a relational database?

我们截断测试期间修改的表以拆除装置。

We truncate the tables modified during the test to tear down the fixture.

图像

确保测试可重复且稳健的很大一部分是确保每次测试后都拆除测试装置。剩余的对象和数据库记录以及打开的文件和连接,在最好的情况下会导致性能下降,在最坏的情况下会导致测试失败或系统崩溃。虽然其中一些资源可能会通过垃圾回收自动清理,但如果没有明确拆除,其他资源可能会被搁置。

A large part of making tests repeatable and robust is ensuring that the test fixture is torn down after each test. Leftover objects and database records, as well as open files and connections, can at best cause performance degradation and at worst cause tests to fail or systems to crash. While some of these resources may be cleaned up automatically by garbage collection, others may be left hanging if they are not torn down explicitly.

编写在所有可能情况下都能够正确清理的拆卸代码是一项艰巨而耗时的挑战。它需要了解测试的每个可能结果可能遗留什么,并编写代码来处理这种可能性。这种复杂的拆卸(请参阅第 186页的模糊测试)引入了相当多的条件测试逻辑第 200页)和最糟糕的不可测试的测试代码(请参阅第 209页的难以测试的代码)。

Writing teardown code that can be relied upon to clean up properly in all possible circumstances is challenging and time-consuming. It involves understanding what could be left over for each possible outcome of the test and writing code to deal with that possibility. This Complex Teardown (see Obscure Test on page 186) introduces a fair bit of Conditional Test Logic (page 200) and—worst of all—Untestable Test Code (see Hard-to-Test Code on page 209).

在测试使用关系数据库的系统时,我们可以利用数据库的功能,使用TRUNCATE命令从我们修改的表中删除所有数据。

When testing a system that uses a relational database, we can take advantage of the database's capabilities by using the TRUNCATE command to remove all data from a table we have modified.

工作原理

How It Works

当我们不再需要持久性装置时,我们TRUNCATE会对装置中的每个表发出一个命令。它会非常有效地将所有数据从表中删除,而不会产生任何副作用(例如触发器)。

When we no longer need a persistent fixture, we issue a TRUNCATE command for each table in the fixture. It blasts all data out of the tables very efficiently with no side effects (e.g., triggers).

何时使用它

When to Use It

当我们对包含数据库的 SUT使用持久新鲜夹具(请参阅第 311页的新鲜夹具)策略时,我们经常会求助于表截断拆卸。但这很少是我们的首选。这种区别在于事务回滚拆卸第 668页)。尽管如此,表截断拆卸是与共享夹具第 317页)一起使用的更好选择,因为这种类型的夹具根据定义比任何一次测试都存活得久。相比之下,将事务回滚拆卸共享夹具一起使用则需要运行非常长时间的事务。虽然并非不可能,但这种长寿命的事务很麻烦。

We often turn to Table Truncation Teardown when we are using a Persistent Fresh Fixture (see Fresh Fixture on page 311) strategy with an SUT that includes a database. It is rarely our first choice, however. That distinction goes to Transaction Rollback Teardown (page 668). Nevertheless, Table Truncation Teardown is a better choice for use with a Shared Fixture (page 317), as this type of fixture, by definition, outlives any one test. By contrast, using Transaction Rollback Teardown with a Shared Fixture would require a very long-running transaction. While not impossible, such a long-lived transaction is troublesome.

在使用Table Truncation Teardown 之前,我们必须满足几个条件。第一个要求是我们确实希望删除受影响表中的所有数据。第二个要求是每个Test Runner(第377页)都有自己的Database Sandbox(第650页)。如果我们使用Database Partitioning Scheme(请参阅Database Sandbox)将用户或测试彼此隔离,则 Table Truncation Teardown将无法工作。它最适合与每个 Test Runner 一个 DB Schema(请参阅Database Sandbox)一起使用,尤其是当我们在数据库中实现Immutable Shared Fixture(请参阅Shared Fixture )作为单独的共享模式时。这使我们能够在自己的Database Sandbox中删除所有Fresh Fixture数据,而不会影响Immutable Shared Fixture

Before we can use Table Truncation Teardown, we must satisfy a couple of criteria. The first requirement is that we really want all data in the affected tables removed. The second requirement is that each Test Runner (page 377) has its own Database Sandbox (page 650). Table Truncation Teardown will not work if we are using a Database Partitioning Scheme (see Database Sandbox) to isolate users or tests from one another. It is ideally suited for use with a DB Schema per Test Runner (see Database Sandbox), especially when we are implementing an Immutable Shared Fixture (see Shared Fixture) as a separate shared schema in the database. This allows us to blast away all the Fresh Fixture data in our own Database Sandbox without affecting the Immutable Shared Fixture.

如果我们不使用事务数据库,最接近的方法是自动拆卸第 503页),它只删除测试创建的记录。自动拆卸不依赖于数据库事务来完成工作,但它确实需要我们做更多的开发工作。我们还可以通过使用Delta Assertions第 485页)完全避免拆卸。

If we are not using a transactional database, the closest approximation is Automated Teardown (page 503), which deletes only those records that were created by the test. Automated Teardown does not depend on the database transactions to do the work for it, but it does involve more development work on our part. We can also avoid the need to do teardown entirely by using Delta Assertions (page 485).

实施说明

Implementation Notes

除了通常的“我们将拆卸代码放在哪里?”的决定之外,表截断拆卸的实现还需要处理以下问题:

Besides the usual "Where do we put the teardown code?" decision, implementation of Table Truncation Teardown needs to deal with the following questions:

  • 我们实际上如何删除数据——也就是说,我们使用哪些数据库命令?
  • How do we actually delete the data—that is, which database commands do we use?
  • 我们如何处理外键约束和触发器?
  • How do we deal with foreign key constraints and triggers?
  • 当我们使用对象关系映射(ORM)时,如何确保一致性?
  • How do we ensure consistency when we are using an object-relational mapping (ORM)?

某些数据库直接支持该TRUNCATE命令。在这种情况下,显然应该使用此命令。例如,Oracle 支持该命令TRUNCATE。否则,我们可能不得不改用DELETE  *  FROM  table-name命令。可以使用In-line Teardown(第509页 — 在每个测试方法中调用;参见第 348页)或Implicit Teardown(第516页— 从方法中调用)发出TRUNCATE或命令。有些人更喜欢将此命令与Lazy Teardown一起使用,因为它可以确保在测试开始时表为空,以防这些表受到无关数据的影响。DELETEtearDown

Some databases support the TRUNCATE command directly. Where this is the case, the obvious choice is to use this command. Oracle, for example, supports TRUNCATE. Otherwise, we may have to use a DELETE  *  FROM  table-name command instead. The TRUNCATE or DELETE commands can be issued using In-line Teardown (page 509—called from within each Test Method; see page 348) or Implicit Teardown (page 516—called from the tearDown method). Some people prefer to use this command with Lazy Teardown because it ensures that the tables are empty at the beginning of the test in cases where those tables would be affected by extraneous data.

如果我们的数据库不提供与 Oracle 类似的选项,数据库外键约束可能会成为表截断拆卸ON  DELETE  CASCADE的问题。在 Oracle 中,如果截断表的命令包含该ON  DELETE  CASCADE选项,那么依赖于被截断表行的行也将被删除。如果我们的数据库不级联删除,我们必须确保按照模式要求的顺序截断表。模式更改可能会使此顺序无效,导致拆卸代码失败。幸运的是,此类故障很容易检测到:测试错误告诉我们拆卸需要调整。更正相当简单 - 通常,我们只需要重新排序命令TRUNCATE。当然,我们可以TRUNCATE根据表之间的依赖关系动态地想出一种方法以正确的顺序发出命令。但通常情况下,将此截断逻辑封装在测试实用程序方法第 599页)后面就足够了。

Database foreign key constraints can be a problem for Table Truncation Teardown if our database does not offer something similar to Oracle's ON  DELETE  CASCADE option. In Oracle, if the command to truncate a table includes the ON  DELETE  CASCADE option, then rows dependent on the truncated table rows are deleted as well. If our database does not cascade deletes, we must ensure that the tables are truncated in the order required by the schema. Schema changes can invalidate this order, resulting in failures in the teardown code. Fortunately, such failures are easy to detect: A test error tells us that our teardown needs adjusting. Correction is fairly straightforward—typically, we just need to reorder the TRUNCATE commands. We could, of course, come up with a way to issue the TRUNCATE commands in the correct order dynamically based on the dependencies between the tables. Usually, however, it is enough to encapsulate this truncation logic behind a Test Utility Method (page 599).

如果我们想避免触发器的副作用以及TRUNCATE不支持的数据库的其他复杂情况,我们可以在测试期间禁用约束和/或触发器。只有当其他测试在约束和触发器到位的情况下运行 SUT 时,我们才应该采取这一步骤。

If we want to avoid the side effects of triggers and other complications for databases where TRUNCATE is not supported, we can disable the constraints and/or triggers for the duration of the test. We should take this step only if other tests exercise the SUT with the constraints and triggers in place.

如果我们使用的是 ORM 层(如 Toplink、(N)Hibernate 或 EJB 3.0),则可能需要强制 ORM 清除已从数据库读取的对象缓存,以便后续的对象查找不会找到最近删除的对象。例如,NHibernateClearAllCaches为此目的提供了方法TransactionManager

If we are using an ORM layer such as Toplink, (N)Hibernate, or EJB 3.0, we may need to force the ORM to clear its cache of objects already read from the database so that subsequent object lookups do not find the recently deleted objects. For example, NHibernate provides the ClearAllCaches method on the TransactionManager for this purpose.

变体:惰性拆卸

只适用于少数几种共享装置的拆卸技术是延迟拆卸。使用这种模式,装置必须在任意时间点可销毁。因此,我们不能依赖“记住”需要拆卸的内容;它必须在没有任何“记忆”的情况下显而易见。表截断拆卸符合要求,因为无论何时我们选择执行拆卸,执行拆卸的方式都是完全相同的。我们只需在设置新装置之前在装置设置期间发出表截断命令即可。

A teardown technique that works with only a few styles of Shared Fixtures is Lazy Teardown. With this pattern, the fixture must be destroyable at an arbitrary point in time. Thus we cannot depend on "remembering" what needs to be torn down; it must be obvious without any "memory." Table Truncation Teardown fits the bill because how we perform teardown is exactly the same whenever we choose to do it. We simply issue the table truncation commands during fixture setup before setting up the new fixture.

激励人心的例子

Motivating Example

以下测试尝试使用保证内联拆卸(请参阅内联拆卸)删除其创建的所有记录:

The following test attempts to use Guaranteed In-line Teardown (see In-line Teardown) to remove all the records it created:

[测试]

public void TestGetFlightsByOrigin_NoInboundFlights()

{

      // 固定设置

      long OutboundAirport = CreateTestAirport("1OF");

      long InboundAirport = CreateTestAirport("1IF");

      FlightDto ExpFlightDto = null;

      try

      {

            ExpFlightDto =

                  CreateTestFlight(OutboundAirport, InboundAirport);

            // 练习系统

            IList FlightsAtDestination1 =

                  Facade.GetFlightsByOriginAirport( InboundAirport);

            // 验证结果

            Assert.AreEqual( 0, FlightsAtDestination1.Count );

      }

      finally

      {

              Facade.RemoveFlight( ExpFlightDto.FlightNumber );

              Facade.RemoveAirport( OutboundAirport );

              Facade.RemoveAirport( InboundAirport );

      }

}

[Test]

public  void  TestGetFlightsByOrigin_NoInboundFlights()

{

      //  Fixture  Setup

      long  OutboundAirport  =  CreateTestAirport("1OF");

      long  InboundAirport  =  CreateTestAirport("1IF");

      FlightDto  ExpFlightDto  =  null;

      try

      {

            ExpFlightDto  =

                  CreateTestFlight(OutboundAirport,  InboundAirport);

            //  Exercise  System

            IList  FlightsAtDestination1  =

                  Facade.GetFlightsByOriginAirport(  InboundAirport);

            //  Verify  Outcome

            Assert.AreEqual(  0,  FlightsAtDestination1.Count  );

      }

      finally

      {

              Facade.RemoveFlight(  ExpFlightDto.FlightNumber  );

              Facade.RemoveAirport(  OutboundAirport  );

              Facade.RemoveAirport(  InboundAirport  );

      }

}

 

这段代码既不容易写,也不正确!1尝试跟踪 SUT 创建的许多对象,然后以安全的方式将它们一个接一个地拆除是非常棘手的。

This code is neither easy to write nor correct!1 Trying to keep track of the many objects the SUT has created and then tear them down one by one in a safe manner is very tricky.

重构说明

Refactoring Notes

通过使用表截断拆卸并一举摧毁所有机场,我们可以安全地避免协调多个资源的内联拆卸的大部分问题。 2大部分重构工作涉及从子句中删除现有的拆卸代码并插入对的调用。然后我们使用截断命令实现此方法。finallycleanDatabase

We can avoid most of the issues with coordinating In-line Teardown of multiple resources in a safe way by using Table Truncation Teardown and blasting away all the airports in one fell swoop.2 Most of the refactoring work involves deleting the existing teardown code from the finally clause and inserting a call to cleanDatabase. We then implement this method using the truncation commands.

示例:表截断(委托)拆卸测试

Example: Table Truncation (Delegated) Teardown Test

完成后的测试如下所示:

This is what the test looks like when we are done:

public void TestGetFlightsByOrigin_NoInboundFlight_TTTD()

{

      // Fixture Setup

      long OutboundAirport = CreateTestAirport("1OF");

      long InboundAirport = 0;

      FlightDto ExpectedFlightDto = null;

      try

      {

            InboundAirport = CreateTestAirport("1IF");

            ExpectedFlightDto =

                  CreateTestFlight( OutboundAirport,InboundAirport);

            // 练习系统

            IList FlightsAtDestination1 =

                  Facade.GetFlightsByOriginAirport(InboundAirport);

            // 验证结果

            Assert.AreEqual(0,FlightsAtDestination1.Count);

      }

      finally

      {

              CleanDatabase();

      }

}

public  void  TestGetFlightsByOrigin_NoInboundFlight_TTTD()

{

      //  Fixture  Setup

      long  OutboundAirport  =  CreateTestAirport("1OF");

      long  InboundAirport  =  0;

      FlightDto  ExpectedFlightDto  =  null;

      try

      {

            InboundAirport  =  CreateTestAirport("1IF");

            ExpectedFlightDto  =

                  CreateTestFlight(  OutboundAirport,InboundAirport);

            //  Exercise  System

            IList  FlightsAtDestination1  =

                  Facade.GetFlightsByOriginAirport(InboundAirport);

            //  Verify  Outcome

            Assert.AreEqual(0,FlightsAtDestination1.Count);

      }

      finally

      {

              CleanDatabase();

      }

}

 

此示例使用委托拆卸(请参阅内联拆卸)使拆卸代码保持可见。但是,通常情况下,我们会通过将此逻辑放入方法中使用隐式拆卸tearDowntry/catch确保cleanDatabase运行,但不能确保内部故障cleanDatabase不会阻止拆卸完成。

This example uses Delegated Teardown (see In-line Teardown) to keep the teardown code visible. Normally, however, we would use Implicit Teardown by putting this logic into the tearDown method. The try/catch ensures that cleanDatabase is run but it does not ensure that a failure inside cleanDatabase will not prevent the teardown from completing.

示例:惰性拆卸测试

Example: Lazy Teardown Test

以下是转换为使用 Lazy Teardown 的相同示例

Here is the same example converted to use Lazy Teardown:

[测试]

public void TestGetFlightsByOrigin_NoInboundFlight_LTD()

{

      // 惰性拆卸

      CleanDatabase();

      // 固定装置设置

      long OutboundAirport = CreateTestAirport("1OF");

      long InboundAirport = 0;

      FlightDto ExpectedFlightDto = null;

      InboundAirport = CreateTestAirport("1IF");

      ExpectedFlightDto =

            CreateTestFlight( OutboundAirport, InboundAirport);

      // 练习系统

      IList FlightsAtDestination1 =

            Facade.GetFlightsByOriginAirport(InboundAirport);

      // 验证结果

      Assert.AreEqual(0,FlightsAtDestination1.Count);

}

[Test]

public  void  TestGetFlightsByOrigin_NoInboundFlight_LTD()

{

      //  Lazy  Teardown

      CleanDatabase();

      //  Fixture  Setup

      long  OutboundAirport  =  CreateTestAirport("1OF");

      long  InboundAirport  =  0;

      FlightDto  ExpectedFlightDto  =  null;

      InboundAirport  =  CreateTestAirport("1IF");

      ExpectedFlightDto  =

            CreateTestFlight(  OutboundAirport,  InboundAirport);

      //  Exercise  System

      IList  FlightsAtDestination1  =

            Facade.GetFlightsByOriginAirport(InboundAirport);

      //  Verify  Outcome

      Assert.AreEqual(0,FlightsAtDestination1.Count);

}

 

通过将调用移到测试方法cleanDatabase的前面我们确保数据库处于我们期望的状态。此代码会清除上一次测试所做的一切,无论该测试是否提供了正确的拆卸。它还会处理自上次测试运行以来添加到相关表的所有内容。它还有一个额外的好处,即消除了对构造的需求try/finally,从而使测试更简单、更容易理解。

By moving the call to cleanDatabase to the front of the Test Method, we ensure that the database is in the state we expect it. This code cleans up whatever the last test did, regardless of whether that test provided proper teardown. It also takes care of anything added to the relevant tables since the last test was run. It has the added benefit of eliminating the need for the try/finally construct, thereby making the test simpler and easier to understand.

示例:使用 SQL 进行表截断拆除

Example: Table Truncation Teardown Using SQL

cleanDatabase方法的实现使用代码中构建的 SQL 语句:

This implementation of the cleanDatabase method uses SQL statements constructed within the code:

公共静态 void CleanDatabase() {

      string[] tablesToTruncate =

          new string[] {"机场","城市","Airline_Cd","航班"};

          IDbConnection conn = getCurrentConnection();

      IDbTransaction txn = conn.BeginTransaction();

      尝试 {

            foreach (string eachTableToTruncate in tablesToTruncate)

            {

                  TruncateTable(txn, eachTableToTruncate);

            }

            txn.Commit();

            conn.Close();

      } catch (Exception e) {

            txn.Rollback();

      } finally {

              conn.Close();

      }

  }

  私有静态 void TruncateTable( IDbTransaction txn,

                                                         string tableName)

  {

        const string C_DELETE_SQL = "从 {0} 删除";



        IDbCommand cmd = txn.Connection.CreateCommand();

        cmd.Transaction = txn;

        cmd.CommandText = 字符串.格式(C_DELETE_SQL,表名);



        cmd.ExecuteNonQuery();

  }

public  static  void  CleanDatabase()  {

      string[]  tablesToTruncate  =

          new  string[]  {"Airport","City","Airline_Cd","Flight"};

          IDbConnection  conn  =  getCurrentConnection();

      IDbTransaction  txn  =  conn.BeginTransaction();

      try  {

            foreach  (string  eachTableToTruncate  in  tablesToTruncate)

            {

                  TruncateTable(txn,  eachTableToTruncate);

            }

            txn.Commit();

            conn.Close();

      }  catch  (Exception  e)  {

            txn.Rollback();

      }  finally  {

              conn.Close();

      }

  }

  private  static  void  TruncateTable(  IDbTransaction  txn,

                                                         string  tableName)

  {

        const  string  C_DELETE_SQL  =  "DELETE  FROM  {0}";



        IDbCommand  cmd  =  txn.Connection.CreateCommand();

        cmd.Transaction  =  txn;

        cmd.CommandText  =  string.Format(C_DELETE_SQL,  tableName);



        cmd.ExecuteNonQuery();

  }

 

因为我们使用 SQL Server 作为数据库,所以我们必须实现自己的TruncateTable发出 SQL 命令的方法Delete  *  from  ...。如果我们的数据库直接实现,我们就不必采取这一步骤TRUNCATE

Because we are using SQL Server as the database, we had to implement our own TruncateTable method that issues a Delete  *  from  ... SQL command. We would not have to take this step if our database implemented TRUNCATE directly.

示例:使用 ORM 进行表截断拆卸

Example: Table Truncation Teardown Using ORM

cleanDatabase以下是使用 ORM 层 NHibernate的方法实现:

Here is the implementation of the cleanDatabase method using NHibernate, an ORM layer:

public static void CleanDatabase() {

      ISession session =

                      TransactionManager.Instance.CurrentSession;

      TransactionManager.Instance.BeginTransaction();

      try {

              // 我们只需要删除根类,因为

              // 级联规则将删除所有相关的子实体

              session.Delete("from Airport");

              session.Delete("from City");

              session.Flush();

              TransactionManager.Instance.Commit();

      } catch (Exception e) {

              Console.Write(e);

              throw e;

      } finally {

                TransactionManager.Instance.CloseSession();

      }

}

public  static  void  CleanDatabase()  {

      ISession  session  =

                      TransactionManager.Instance.CurrentSession;

      TransactionManager.Instance.BeginTransaction();

      try  {

              //  We  need  to  delete  only  the  root  classes  because

              //  cascade  rules  will  delete  all  related  child  entities

              session.Delete("from  Airport");

              session.Delete("from  City");

              session.Flush();

              TransactionManager.Instance.Commit();

      }  catch  (Exception  e)  {

              Console.Write(e);

              throw  e;

      }  finally  {

                TransactionManager.Instance.CloseSession();

      }

}

 

使用 ORM 时,我们会读取、写入和删除域对象;工具会确定它们映射到哪些底层表并采取适当的操作。由于我们已选择创建CityAirport根”(父)对象,因此当删除根对象时,会自动删除任何下属(子)对象(例如)Flights。这种方法进一步将我们与表实现的细节分离开来。

When using an ORM, we read, write, and delete domain objects; the tool determines which underlying tables they map to and takes the appropriate actions. Because we have chosen to make City and Airport "root" (parent) objects, any subordinate (child) objects such as the Flights are deleted automatically when the root is deleted. This approach further decouples us from the details of the table implementations.

事务回滚拆除

Transaction Rollback Teardown

当测试装置位于关系数据库中时,我们如何拆除它?

How do we tear down the Test Fixture when it is in a relational database?

我们将未提交的测试事务作为拆除的一部分回滚。

We roll back the uncommitted test transaction as part of the teardown.

图像

确保测试可重复且稳健的很大一部分是确保每次测试后都拆除测试装置。剩余的对象和数据库记录以及打开的文件和连接,在最好的情况下会导致性能下降,在最坏的情况下会导致测试失败或系统崩溃。虽然其中一些资源可能会通过垃圾回收自动清理,但如果没有明确拆除,其他资源可能会被搁置。

A large part of making tests repeatable and robust is ensuring that the test fixture is torn down after each test. Leftover objects and database records, as well as open files and connections, can at best cause performance degradation and at worst cause tests to fail or systems to crash. While some of these resources may be cleaned up automatically by garbage collection, others may be left hanging if they are not torn down explicitly.

编写在所有可能情况下都能够正确清理的拆卸代码是一项艰巨而耗时的挑战。它需要了解测试的每个可能结果可能遗留什么,并编写代码来处理这种情况。这种复杂的拆卸(请参阅第 186页的模糊测试)引入了相当多的条件测试逻辑第 200页)和最糟糕的不可测试的测试代码(请参阅第 209页的难以测试的代码)。

Writing teardown code that can be relied upon to clean up properly in all possible circumstances is challenging and time-consuming. It involves understanding what could be left over for each possible outcome of the test and writing code to deal with this case. This Complex Teardown (see Obscure Test on page 186) introduces a fair bit of Conditional Test Logic (page 200) and—worst of all—Untestable Test Code (see Hard-to-Test Code on page 209).

我们可以通过不提交事务并利用数据库的回滚功能来避免对数据库内容进行任何持久更改。

We can avoid making any lasting changes to the database contents by not committing the transaction and taking advantage of the rollback capabilities of the database.

工作原理

How It Works

我们的测试启动一个新的测试事务,设置夹具,执行 SUT,并验证测试结果。这些步骤中的每一个都可能涉及与数据库交互。在测试结束时,测试会回滚测试事务,从而防止任何更改成为持久的。

Our test starts a new test transaction, sets up the fixture, exercises the SUT, and verifies the outcome of the test. Each of these steps may involve interacting with the database. At the end of the test, the test rolls back the test transaction, which prevents any of the changes from becoming persistent.

何时使用它

When to Use It

当我们使用Fresh Fixture (第 311页) 方法,且 SUT 包含支持回滚事务的数据库时,我们可以使用事务回滚拆卸。但是,使用事务回滚拆卸有一些先决条件。

We can use Transaction Rollback Teardown when we are using a Fresh Fixture (page 311) approach with an SUT that includes a database that supports rolling back a transaction. There are, however, some prerequisites for using Transaction Rollback Teardown.

具体来说,SUT 必须公开通常由Humble Transaction Controller在现有事务上下文中调用的方法(请参阅第 695页的Humble Object)。也就是说,这些方法不应启动自己的事务,也绝不能提交事务。如果我们进行测试驱动开发,则此设计将在编写代码时应用事务回滚拆卸模式的结果。如果我们要对现有软件进行测试改造,则可能需要重构代码以使用Humble Transaction Controller,然后才能使用事务回滚拆卸

In particular, the SUT must expose methods that are normally called in the context of an existing transaction by a Humble Transaction Controller (see Humble Object on page 695). That is, the methods should not start their own transaction and must never commit a transaction. If we are doing test-driven development, this design will come about as a result of applying the Transaction Rollback Teardown pattern as we write our code. If we are retrofitting the tests to existing software, we may need to refactor the code to use a Humble Transaction Controller before we can use Transaction Rollback Teardown.

事务回滚拆卸的优点在于,无论我们在测试期间对数据库内容做了哪些更改,它都会使数据库保持与我们开始测试时完全相同的状态。因此,我们不需要确定哪些需要清理,哪些不需要。对数据库架构或内容的更改不会影响我们的拆卸逻辑。显然,这种模式比表截断拆卸第 661页)更容易应用。

The nice thing about Transaction Rollback Teardown is that it leaves the database in exactly the same state as it was when we started the test, regardless of what changes we made to the database contents during the test. As a result, we do not need to determine what needs to be cleaned up and what does not. Changes to the database schema or contents do not affect our teardown logic. Clearly, this pattern is much simpler to apply than Table Truncation Teardown (page 661).

常见的注意事项适用于针对真实数据库运行的任何测试;此类测试的运行时间大约是未访问数据库的测试的 50 倍(是的,50 倍!)。除非我们在大多数测试中将真实数据库替换为内存数据库(请参阅第 551 页的“伪对象”),否则这种测试方法几乎肯定会导致测试速度变慢(第 253因为我们依赖数据库的事务属性,所以简单的伪数据库(请参阅“伪对象”)可能不够用,除非它支持ACID

The usual caveats apply to any tests that run against a real database; such tests will take approximately 50 (yes, 50!) times as long to run as tests that do not access the database. This testing approach will almost surely result in Slow Tests (page 253) unless we replace the real database with an In-Memory Database (see Fake Object on page 551) for most of our tests. Because we are depending on the transactional properties of the database, a simple Fake Database (see Fake Object) will probably not be sufficient unless it supports ACID.

事务回滚拆卸的另一个先决条件是,我们不能在测试或它们执行的代码中的任何位置执行任何导致提交的操作。第 670页的侧栏“事务回滚之痛”描述了提交可能潜入并造成破坏的示例。

Another prerequisite with Transaction Rollback Teardown is that we cannot do anything that results in a commit anywhere in the tests or the code they exercise. The sidebar "Transaction Rollback Pain" on page 670 describes examples of where commits can sneak in and cause havoc.


交易回滚之痛

John Hurst 给我发了一封电子邮件,描述了他的团队在使用Transaction Rollback Teardown 时遇到的一些问题。他写道:

在 TheServerSide 上讨论后,Rod Johnson 提倡使用事务回滚拆卸方法,之后我们曾一度将事务回滚拆卸用于数据库集成测试。我了解到他使用该方法的主要动机是为了提高性能;回滚通常比在下一个测试的新事务中重新启动数据库要快得多。事实上,我们发现它比我们以前的方法要快一些。我们使用了 Spring 的优秀 AbstractTransactionalDataSourceSpringContextTests 基类,它开箱即用地支持您需要为该模式执行的大部分操作。

 

然而,几个月后我选择放弃这种模式。以下是我在这种方法中发现的缺点:

 
  1. 您会失去一些测试隔离。无论如何,在我们实现此模式的方式中,每个测试都假设数据库处于某个基本起始条件,而回滚会将其恢复到该条件。在我们当前的模型中,每个测试都负责(通常通过基类 setUp()将数据库设置为已知状态。
  2. 当出现问题时,您无法看到数据库中的内容。如果测试失败,您通常希望检查数据库以查看发生了什么。如果您已回滚所有更改,则很难找到错误。
  3. 您必须非常小心,不要在测试期间无意中提交。是的,测试代码具有声明式事务管理,并且不会做任何令人惊讶的事情。但我们偶尔需要在测试设置中执行一些操作,例如删除并重新创建序列以重置其值。这是 DDL,会提交任何未完成的事务,并让程序员感到困惑。
  4. 您不能轻易地混入需要提交更改的测试。最近我添加了一些 PLSQL 存储过程和测试。一些存储过程执行显式提交。我不能将它们与假设数据库始终保持特定状态的测试混合在同一个 JUnit 套件中。

如果我的术语与您的书中不一致,我深表歉意。另外,我的经验可能有些有限;我只在 Spring 环境中尝试过这种方法,而且我更喜欢以“Spring”方式做大多数事情。最后,我确信这些限制可以通过各种方式解决。只是,对于我们的团队来说,这种模式带来的麻烦比它的价值更大。

别误会我的意思——我确实认为应该包括这种模式。我只是认为应该注意后果,也许并不适合所有人。



Transaction Rollback Pain

John Hurst sent me an e-mail in which he described some of the issues his team had encountered using Transaction Rollback Teardown. He writes:

We used Transaction Rollback Teardown for our database integration tests for a while, after a discussion on TheServerSide during which Rod Johnson advocated the approach. I gathered his main motivation for using it was for performance; a rollback is usually a lot faster than repriming the database in a new transaction for the next test. Indeed, we did find it somewhat faster than our previous approach. We used Spring's excellent AbstractTransactionalDataSourceSpringContextTests base class, which supports most of what you need to do for this pattern out of the box.

 

However, I chose to abandon this pattern after a few months. Here are the drawbacks I came across with this approach:

 
  1. You lose some test isolation. In the way we implemented this pattern, anyway, each test assumed the database was in a certain base starting condition, and the rollback would revert it to that condition. In our current model, each test is responsible—usually via a base class's setUp()for priming the database into a known state.
  2. You can't see what's in the database when something goes wrong. If your test fails, you usually want to examine the database to see what happened. If you've rolled back all the changes, it makes it harder to find the bug.
  3. You have to be very careful not to inadvertently commit during your test. Yes, the code under test has declarative transaction management, and does nothing surprising. But we occasionally would need to do things in the test setup like drop and recreate a sequence to reset its value. This, being DDL, commits any outstanding transaction—and confused programmers.
  4. You can't easily mix in tests that do need to commit changes. Lately I have added some PLSQL stored procedures and tests. Some of the stored procedures do explicit commits. I cannot mix these in the same JUnit suite with tests that assume the database always remains in a certain state.

I apologize if my terminology isn't consistent with what's in your book. Also, my experience is probably a little limited; I've only tried this approach in a Spring environment and I prefer to do most things in a "Spring" way. Finally, I am sure these limitations can be and are worked around in various ways. It's just that, for our team, this pattern turned out to be more trouble than it was worth.

Don't get me wrong—I DO think the pattern should be included. I just think the consequences should be noted, and maybe it isn't for everyone.


 

实施说明

Implementation Notes

xUnit 系列的少数成员直接支持事务回滚拆卸;其他成员可能有开源扩展。如果没有可用扩展,编写此拆卸逻辑也不太复杂。更重要的实现考虑是让测试访问 SUT 上的非事务方法。大多数领域模型对象都是非事务性的,因此这个要求对于领域对象的单元测试来说应该不是问题。我们在针对服务外观[CJ2EEP]编写皮下测试(请参阅第 337页的层测试)时更有可能遇到问题,因为这些方法通常执行事务控制。如果是这种情况,我们需要通过重构为谦逊事务控制器模式来公开方法的非事务版本。我们可以使用事务装饰器[GOF]作为单独的对象,也可以简单地让事务方法委托给方法的非事务版本。这种方法称为穷人的谦逊对象(请参阅谦逊对象)。self

A few members of the xUnit family support Transaction Rollback Teardown directly; open-source extensions may be available for other members. If nothing is available, coding this teardown logic is not very complicated. The more significant implementation consideration is giving the tests access to nontransactional methods on the SUT. Most domain model objects are nontransactional, so this requirement should not be a problem for unit tests of domain objects. We are more likely to experience a problem when we are writing Subcutaneous Tests (see Layer Test on page 337) against a Service Facade [CJ2EEP] because these methods often perform transaction control. If this is the case, we will need to expose a nontransactional version of the methods by refactoring to the Humble Transaction Controller pattern. We could either use a transactional Decorator [GOF] as a separate object or simply have the transactional methods delegate to the nontransactional versions of the methods on self. This approach is called a Poor Man's Humble Object (see Humble Object).

如果方法存在但对客户端不可见,则需要将它们公开给测试。我们可以通过公开要测试的方法或通过测试专用子类(第 579页) 间接公开它们来实现这一点。我们还可以进行提取可测试组件 (第735页) 重构,将方法的非事务版本移至不同的类,并从那里使它们对测试可见。

If the methods exist but are not visible to the client, we will need to expose them to the test. We can do so either by making the methods to be tested public or by exposing them indirectly via a Test-Specific Subclass (page 579). We could also do an Extract Testable Component (page 735) refactoring to move the nontransactional versions of the methods to a different class and make them visible to the test from there.

对数据库中更新数据的任何读取都必须在同一事务的上下文中进行。这通常不是问题,除非我们试图模拟或测试并发性。如果我们使用 ORM 层(如 Toplink、(N)Hibernate 或 EJB 3.0),我们可能需要强制 ORM 将对对象所做的更改写入数据库,以便直接读取数据库的方法(从同一事务上下文中)可以看到它们。例如,EJB 3.0 提供了EntityManager.flush静态方法来实现此目的。

Any reading of the updated data in the database must occur within the context of the same transaction. This normally is not a problem except when we are trying to simulate or test concurrency. If we are using an ORM layer such as Toplink, (N)Hibernate, or EJB 3.0, we may need to force the ORM to write the changes made to the objects to the database so that methods that read the database directly (from within the same transactional context) can see them. For example, EJB 3.0 provides the EntityManager.flush static method for exactly this purpose.

激励人心的例子

Motivating Example

以下测试尝试使用保证内联拆卸(请参阅第509页的内联拆卸)删除其创建的所有记录:

The following test attempts to use Guaranteed In-line Teardown (see In-line Teardown on page 509) to remove all the records it created:

public void testGetFlightsByOriginAirport_NoInboundFlights()

              throws Exception {

      // Fixture Setup

      BigDecimal outboundAirport = createTestAirport("1OF");

      BigDecimal inboundAirport = createTestAirport("1IF");

      FlightDto expFlightDto = null;

      try {

            expFlightDto = createTestFlight(outboundAirport, inboundAirport);

            // 练习系统

            列表 flightsAtDestination1 =

                      Facade.getFlightsByOriginAirport( inboundAirport);

            // 验证结果

            assertEquals( 0, flightsAtDestination1.size() );

    } finally {

            Facade.removeFlight( expFlightDto.getFlightNumber() );

            Facade.removeAirport( outboundAirport );

            Facade.removeAirport( inboundAirport );

    }

}

public  void  testGetFlightsByOriginAirport_NoInboundFlights()

              throws  Exception  {

      //  Fixture  Setup

      BigDecimal  outboundAirport  =  createTestAirport("1OF");

      BigDecimal  inboundAirport  =  createTestAirport("1IF");

      FlightDto  expFlightDto  =  null;

      try  {

            expFlightDto  =  createTestFlight(outboundAirport,  inboundAirport);

            //  Exercise  System

            List  flightsAtDestination1  =

                      facade.getFlightsByOriginAirport(  inboundAirport);

            //  Verify  Outcome

            assertEquals(  0,  flightsAtDestination1.size()  );

    }  finally  {

            facade.removeFlight(  expFlightDto.getFlightNumber()  );

            facade.removeAirport(  outboundAirport  );

            facade.removeAirport(  inboundAirport  );

    }

}

 

这段代码既不容易写,也不正确!3尝试跟踪 SUT 创建的所有对象,然后以安全的方式将它们逐一拆除是非常棘手的。

This code is neither easy to write nor correct!3 Trying to keep track of all objects the SUT has created and then tear them down one by one in a safe manner is very tricky.

重构说明

Refactoring Notes

通过使用事务回滚拆卸并一次性清除对对象的所有更改,我们可以避免与以安全的方式协调多个资源的内联拆卸相关的大多数问题。大部分重构工作包括从子句中删除现有的拆卸代码并插入对方法的调用。我们还需要在进行任何装置设置之前进行调用,并且我们必须修改创建方法(第415页)以确保它们不会提交事务。为此,我们让它们调用服务外观上方法的非事务版本。finallyabortTransactionbeginTransaction

We can avoid most of the issues related to coordinating In-line Teardown of multiple resources in a safe way by using Transaction Rollback Teardown and blasting away all changes to the objects in one fell swoop. Most of the refactoring work consists of deleting the existing teardown code from the finally clause and inserting a call to the abortTransaction method. We also need to make the call to beginTransaction before we do any fixture setup, and we have to modify the Creation Methods (page 415) to ensure that they do not commit a transaction. To do so, we have them call a nontransactional version of the methods on the Service Facade.

示例:对象事务回滚拆卸

Example: Object Transaction Rollback Teardown

完成后的测试如下所示:

Here is what the test looks like when we are done:

public void testGetFlightsByOrigin_NoInboundFlight_TRBTD()

              throws Exception {

      // Fixture Setup

      TransactionManager.beginTransaction();

      BigDecimal outboundAirport = createTestAirport("1OF");

      BigDecimal inboundAirport = null;

      FlightDto expectedFlightDto = null;

      try {

            inboundAirport = createTestAirport("1IF");

            expectedFlightDto =

                  createTestFlight( outboundAirport, inboundAirport);

            // 练习系统

            列表 flightsAtDestination1 =

                Facade.getFlightsByOriginAirport(inboundAirport);

            // 验证结果

            assertEquals(0,flightsAtDestination1.size());

    } finally {

           TransactionManager.abortTransaction();

    }

}

public  void  testGetFlightsByOrigin_NoInboundFlight_TRBTD()

              throws  Exception  {

      //  Fixture  Setup

      TransactionManager.beginTransaction();

      BigDecimal  outboundAirport  =  createTestAirport("1OF");

      BigDecimal  inboundAirport  =  null;

      FlightDto  expectedFlightDto  =  null;

      try  {

            inboundAirport  =  createTestAirport("1IF");

            expectedFlightDto  =

                  createTestFlight(  outboundAirport,  inboundAirport);

            //  Exercise  System

            List  flightsAtDestination1  =

                facade.getFlightsByOriginAirport(inboundAirport);

            //  Verify  Outcome

            assertEquals(0,flightsAtDestination1.size());

    }  finally  {

           TransactionManager.abortTransaction();

    }

}

 

在这个重构测试中,我们将子句中的多行拆卸代码替换finally为对的单个调用abortTransaction。我们仍然需要该finally子句,因为此示例使用的是内联拆卸;我们可以轻松地将此调用移至TransactionManager方法tearDown,因为它非常通用。

In this refactored test, we have replaced the multiple lines of teardown code in the finally clause with a single call to abortTransaction. We still need the finally clause because this example is using In-line Teardown; we could easily move this call to the TransactionManager to the tearDown method because it is so generic.

在此示例中,事务回滚拆卸撤消了我们在测试中先前调用的各种创建方法执行的装置设置。装置对象尚未提交到数据库。getFlightsFromAirport但是,由于是在事务上下文中调用的,因此它返回新添加但尚未提交的航班。(这就是 ACID 中代表“一致”的“C”为我们工作!)

In this example, Transaction Rollback Teardown undoes the fixture setup performed by the various Creation Methods we called earlier in the test. The fixture objects have not yet been committed to the database. Because getFlightsFromAirport is being called within the context of the transaction, however, it returns the newly added but not yet committed flights. (That is the "C" for "consistent" in ACID working on our behalf!)

私有 BigDecimal createTestAirport(String airportName)

              抛出 FlightBookingException {

      BigDecimal newAirportId =

              Facade._createAirport( airportName,

                                                 " Airport" + airportName,

                                                 "City" + airportName);

      返回 newAirportId;

}

private  BigDecimal  createTestAirport(String  airportName)

              throws  FlightBookingException  {

      BigDecimal  newAirportId  =

              facade._createAirport(  airportName,

                                                 "  Airport"  +  airportName,

                                                 "City"  +  airportName);

      return  newAirportId;

}

 

创建方法调用外观方法的非事务版本(穷人的谦卑对象的示例):

The creation method calls the nontransactional version of the facade method (an example of a Poor Man's Humble Object):

public BigDecimal createAirport( String airportCode,

                                                   String name,

                                                   String vicinityCity)

              throws FlightBookingException{

      TransactionManager.beginTransaction();

      BigDecimal airportId = _createAirport(airportCode, name, vicinityCity);

      TransactionManager.commitTransaction();

      return airportId;

}



// 供测试使用的私有非事务版本

BigDecimal _createAirport( String airportCode,

                                           String name,

                                           String vicinityCity)

              throws DataException, InvalidArgumentException {

      Airport airport =

              dataAccess.createAirport(airportCode,name,nearbyCity);

      logMessage("CreateFlight", airport.getCode());

      return airport.getId();

}

public  BigDecimal  createAirport(  String  airportCode,

                                                   String  name,

                                                   String  nearbyCity)

              throws  FlightBookingException{

      TransactionManager.beginTransaction();

      BigDecimal  airportId  =  _createAirport(airportCode,  name,  nearbyCity);

      TransactionManager.commitTransaction();

      return  airportId;

}



//  private,  nontransactional  version  for  use  by  tests

BigDecimal  _createAirport(  String  airportCode,

                                           String  name,

                                           String  nearbyCity)

              throws  DataException,  InvalidArgumentException  {

      Airport  airport  =

              dataAccess.createAirport(airportCode,name,nearbyCity);

      logMessage("CreateFlight",  airport.getCode());

      return  airport.getId();

}

 

如果我们正在执行的方法(例如getFlightsFromAirport确实修改了 SUT 的状态并开始和结束其自己的事务,那么我们也必须对其进行类似的重构。

If the method we were exercising (e.g., getFlightsFromAirport) did modify the state of the SUT and did begin and end its own transaction, we would have to do a similar refactoring on it as well.

示例:数据库事务回滚拆除

Example: Database Transaction Rollback Teardown

第一个例子将数据库隐藏在返回或接受对象的数据访问层后面的代码中。当使用域模型[PEAA]模式来组织业务逻辑时,这是常见的做法。事务回滚拆卸通常用于直接在我们的应用程序逻辑中操作数据库(一种称为事务脚本[PEAA]的样式)。以下示例使用 .NET 行集(或类似内容)说明了这种方法:

The first example hid the database from the code behind a data access layer that returned or accepted objects. This is common practice when using the Domain Model [PEAA] pattern for organizing the business logic. Transaction Rollback Teardown is typically used when manipulating the database directly in our application logic (a style known as a Transaction Script [PEAA]). The following example illustrates this approach using .NET row sets (or something similar):

        [TestFixture]

        public class TransactionRollbackTearDownTest

        {

              private SqlConnection _Connection;

              private SqlTransaction _Transaction;



              public TransactionRollbackTearDownTest()

              {

              }



              [SetUp]

        public void Setup()

        {

              string dbConnectionString = ConfigurationSettings.

                                              AppSettings.Get("DbConnectionString");

              _Connection = new SqlConnection(dbConnectionString);

              _Connection.Open();

              _Transaction = _Connection.BeginTransaction();

        }



        [TearDown]

        public void TearDown()

        {

              _Transaction.Rollback();

              _Connection.Close();

              // 避免 NUnit“实例行为”错误

              _Transaction = null;

              _Connection = null;

        }



        [测试]

        public void AnNUnitTest()

        {

              const string C_INSERT_SQL =

                    "INSERT INTO Invoice(Amount, Tax, CustomerId)" +

                    " VALUES({0}, {1}, {2})";

              SqlCommand cmd = _Connection.CreateCommand();

              cmd.Transaction = _Transaction;

              cmd.CommandText = string.Format(

                                          C_INSERT_SQL,

                                          new object[] {"100.00", "7.00", 2001});

              // 练习 SUT

              cmd.ExecuteNonQuery();

              // 验证结果

              // 等等

        }

    }

}

        [TestFixture]

        public  class  TransactionRollbackTearDownTest

        {

              private  SqlConnection  _Connection;

              private  SqlTransaction  _Transaction;



              public  TransactionRollbackTearDownTest()

              {

              }



              [SetUp]

        public  void  Setup()

        {

              string  dbConnectionString    =  ConfigurationSettings.

                                              AppSettings.Get("DbConnectionString");

              _Connection  =  new  SqlConnection(dbConnectionString);

              _Connection.Open();

              _Transaction  =  _Connection.BeginTransaction();

        }



        [TearDown]

        public  void  TearDown()

        {

              _Transaction.Rollback();

              _Connection.Close();

              //  Avoid  NUnit  "instance  behavior"  bug

              _Transaction  =  null;

              _Connection  =  null;

        }



        [Test]

        public  void  AnNUnitTest()

        {

              const  string  C_INSERT_SQL  =

                    "INSERT  INTO  Invoice(Amount,  Tax,  CustomerId)"  +

                    "  VALUES({0},  {1},  {2})";

              SqlCommand  cmd  =  _Connection.CreateCommand();

              cmd.Transaction  =  _Transaction;

              cmd.CommandText  =  string.Format(

                                          C_INSERT_SQL,

                                          new  object[]  {"100.00",  "7.00",  2001});

              //  Exercise  SUT

              cmd.ExecuteNonQuery();

              //  Verify  result

              //      etc.

        }

    }

}

 

此示例使用隐式设置(第 424页) 建立连接并启动事务。测试方法(第 348页) 运行后,它使用隐式拆卸(第516页) 回滚事务并关闭连接。我们将其分配null给实例变量,因为与 xUnit 的大多数其他成员不同,NUnit 不会为每个测试方法创建单独的测试用例对象(第 382页)。有关详细信息,请参阅第384页的侧栏“始终存在异常” 。

This example uses Implicit Setup (page 424) to establish the connection and start the transaction. After the Test Method (page 348) has run, it uses Implicit Teardown (page 516) to roll back the transaction and close the connection. We assign null to the instance variables because NUnit does not create a separate Testcase Object (page 382) for each Test Method, unlike most other members of xUnit. See the sidebar "There's Always an Exception" on page 384 for details.

第 26 章

可测试性设计模式

Chapter 26

Design-for-Testability Patterns

 

本章中的模式

Patterns in This Chapter

依赖注入 678

Dependency Injection 678

依赖项查找 686

Dependency Lookup 686

卑微的物体 695

Humble Object 695

测试钩 709

Test Hook 709

依赖注入

Dependency Injection

我们如何设计 SUT 以便我们可以在运行时替换它的依赖项?

How do we design the SUT so that we can replace its dependencies at runtime?

客户端向SUT提供所依赖的对象。

The client provides the depended-on object to the SUT.

图像

几乎每一段代码都依赖于其他一些类、对象、模块或过程。为了正确地对一段代码进行单元测试,我们希望将代码与其依赖项隔离开来。如果这些依赖项以文字类名的形式进行硬编码,则很难实现这种隔离。

Almost every piece of code depends on some other classes, objects, modules, or procedures. To unit-test a piece of code properly, we would like to isolate the code from its dependencies. This isolation is difficult to achieve if those dependencies are hard-coded in the form of literal classnames.

依赖注入是一种在自动化测试期间打破 SUT 与其依赖项之间的正常耦合的方法。

Dependency Injection is a way to allow the normal coupling between a SUT and its dependencies to be broken during automated testing.

工作原理

How It Works

我们通过为客户端或系统配置提供其他方式来告诉 SUT 在执行每个依赖项时要使用哪些对象,从而避免将所依赖的类的名称硬编码到代码中。作为 SUT 设计的一部分,我们安排通过“前门”将依赖项传递给 SUT。也就是说,指定依赖项的方法成为 SUT API 的一部分。我们可以将其作为参数包含在每次方法调用中,将其包含在构造函数中,或使其成为可设置的属性(属性)。

We avoid hard-coding the names of classes on which we depend into our code by providing some other means for the client or system configuration to tell the SUT which objects to use for each dependency as it is executed. As part of the design of the SUT, we arrange to pass the dependency in to the SUT through the "front door." That is, the means to specify the dependency becomes part of the API of the SUT. We can include it as an argument with each method call, include it on the constructor, or make it a settable attribute (property).

何时使用它

When to Use It

我们需要提供一种方法来替代依赖组件 (DOC),以便在测试代码时轻松使用测试替身第 522页)。静态绑定(即在编译时指定确切的类型或类)严重限制了我们在软件运行时配置方式的选项。动态绑定将决定使用哪种类型或类的延迟到运行时,从而创建更灵活的软件。当我们从头设计软件时,依赖注入是告知使用哪个类的最佳选择。当我们进行测试驱动开发 (TDD) 时,它提供了一种自然的代码设计方式,因为我们为依赖对象编写的许多测试都试图用测试替身替换 DOC 。

We need to provide a means to substitute a depended-on component (DOC) to make it easy to use a Test Double (page 522) while testing our code. Static binding—that is, specifying exact types or classes at compile time—severely limits our options regarding how the software is configured as it runs. Dynamic binding creates much more flexible software by deferring the decision of exactly which type or class to use until runtime. Dependency Injection is a good choice for communicating which class to use when we are designing the software from scratch. It offers a natural way to design the code when we are doing test-driven development (TDD) because many of the tests we write for dependent objects seek to replace a DOC with a Test Double.

当我们无法完全控制正在测试的代码时,例如当我们将测试改进到现有代码中时,1我们可能需要使用其他方法来引入测试替身。如果 SUT 使用依赖查找第 686页)来查找 DOC,我们可以覆盖查找机制以返回测试替身。我们还可以使用SUT 的测试特定子类(第 579页)来返回测试替身,只要对 DOC 的访问仍然封装在方法调用后面即可。

When we don't have complete control over the code we are testing, such as when we are retrofitting tests to existing code,1 we may need to use some other means to introduce the Test Doubles. If the SUT uses Dependency Lookup (page 686) to find the DOC, we can override the lookup mechanism to return the Test Double. We can also use a Test-Specific Subclass (page 579) of the SUT to return a Test Double as long as access to the DOC remains encapsulated behind a method call.

实施说明

Implementation Notes

引入依赖注入需要解决两个问题。首先,我们必须能够在使用真实 DOC 的任何地方使用测试替身。这个限制主要是静态类型语言中的一个问题,因为我们必须说服编译器允许我们将测试替身当作真实的东西。其次,我们必须提供一种方法来告诉 SUT 使用测试替身

Introducing Dependency Injection requires solving two problems. First, we must be able to use a Test Double wherever the real DOC is used. This constraint is primarily an issue in statically typed languages because we must convince the compiler to allow us to pass off a Test Double as the real thing. Second, we must provide a way to tell the SUT to use the Test Double.

类型兼容性

无论我们选择哪种方式将依赖项安装到 SUT 中,我们还必须确保要替换它的测试替身与使用测试替身的代码“类型兼容” 。如果真实组件和测试替身都实现相同的接口(在静态类型语言中)或具有相同的签名(在动态类型语言中),则最容易实现这一点。将测试替身引入现有代码的一种快速方法是对真实 DOC 进行提取接口 [Fowler] 重构,然后让测试替身实现新接口。

Whichever way we choose to install the dependency into the SUT, we must also ensure that the Test Double we want to replace it with is "type compatible" with the code that uses the Test Double. This is most easily accomplished if both the real component and the Test Double implement the same interface (in statically typed languages) or have the same signature (in dynamically typed languages). A quick way to introduce a Test Double into existing code is to do an Extract Interface [Fowler] refactoring on the real DOC and then have the Test Double implement the new interface.

安装测试替身

有多种不同的方法可以告诉 SUT 使用测试替身,但它们都涉及用确定执行时要使用的对象类型的机制替换硬编码名称。三种基本选项如下:

There are a number of different ways to tell the SUT to use the Test Double, but they all involve replacing a hard-coded name with a mechanism that determines the type of object to use at execution time. The three basic options are as follows:

  • 参数注入:我们在调用依赖项时将依赖项直接传递给 SUT。
  • Parameter Injection: We pass the dependency directly to the SUT as we invoke it.
  • 构造函数注入:我们告诉 SUT 在构造它时使用哪个 DOC。
  • Constructor Injection: We tell the SUT which DOC to use when we construct it.
  • Setter 注入:我们在构建 DOC 和执行 DOC 之间将有关 DOC 的信息告知 SUT。
  • Setter Injection: We tell the SUT about the DOC sometime between when we construct it and when we exercise it.

依赖注入的这三种变体都可以手动编码。另一种选择是使用“控制反转”(IoC) 框架在运行时将各种组件链接在一起。此方案避免了依赖注入在整个应用程序中实现方式的多余多样性,并且可以简化针对不同部署模型重新配置应用程序的过程。

Each of these three variations of Dependency Injection can be hand-coded. Another option is to use an "Inversion of Control" (IoC) framework to link the various components together at runtime. This scheme avoids superfluous diversity in how Dependency Injection is implemented across the application and can simplify the process of reconfiguring the application for different deployment models.

变体:参数注入

参数注入是依赖注入的一种形式,其中 SUT 不保留或初始化对 DOC 的引用;相反,它作为在 SUT 上调用的方法的参数传入。SUT 的所有客户端(无论是测试还是生产代码)都提供 DOC。因此,SUT 更加独立于上下文,因为它除了使用接口之外不对依赖关系做出任何假设。主要缺点是参数注入强制客户端了解依赖关系,这在某些情况下比在其他情况下更合适。大多数其他依赖注入变体将这些知识转移到客户端以外的其他地方,或者至少使其成为可选项。

Parameter Injection is a form of Dependency Injection in which the SUT does not keep or initialize a reference to the DOC; instead, it is passed in as an argument of the method being called on the SUT. All clients of the SUT—whether they are tests or production code—supply the DOC. As a consequence, the SUT is more independent of the context because it makes no assumptions about the dependency other than its usage interface. The main drawback is that Parameter Injection forces the client to know about the dependency, which is more appropriate in some circumstances than in others. Most of the other variants of Dependency Injection move this knowledge somewhere other than the client or at least make it optional.

参数注入是原始论文《Mock Objects》第 544页) [ET]所倡导的。当我们进行真正的 TDD 时,它尤其有效,因为那时我们对设计拥有最大的控制权。可以通过为带有额外参数的相关方法提供替代签名,以可选方式引入参数注入;然后,我们可以让更传统风格的方法创建依赖项的实例,并调用以依赖项为参数的方法。

Parameter Injection is advocated by the original paper on Mock Objects (page 544) [ET]. It is especially effective when we are doing true TDD because that's when we have the greatest control over the design. It is possible to introduce Parameter Injection in an optional fashion by providing an alternative signature for the method in question with the extra parameter; we can then have the more traditional style method create the instance of the dependency and call the method that takes the dependency as a parameter.

变体:构造函数注入

构造函数注入Setter 注入都涉及将对 DOC 的引用存储为 SUT 的属性(字段或实例变量)。使用依赖项注入,字段从构造函数参数初始化。SUT 可以选择提供一个更简单的构造函数,该构造函数使用生产中通常使用的值调用此构造函数。当测试想要用测试替身替换真实 DOC 时,它会在构建 SUT 时将测试替身传递给构造函数。

Both Constructor Injection and Setter Injection involve storing a reference to the DOC as an attribute (field or instance variable) of the SUT. With Dependency Injection, the field is initialized from a constructor argument. The SUT may optionally provide a simpler constructor that calls this constructor with the value normally used in production. When a test wants to replace the real DOC with a Test Double, it passes in the Test Double to the constructor when it builds the SUT.

当代码仅包含一个或两个构造函数且它们的参数列表较小时,这种引入依赖注入的方法效果很好。如果 DOC 是一个在构造期间创建自己的执行线程的活动对象,则构造函数注入唯一有效的方法;这种行为会导致难以测试的代码第 209页),我们可能应该考虑将其转换为Humble 可执行文件(请参阅第 695页的Humble 对象)。如果我们有大量依赖项作为构造函数参数,我们可能需要重构代码以消除这种代码异味。

This approach to introducing Dependency Injection works well when the code includes only one or two constructors and they have small argument lists. Constructor Injection is the only approach that works if the DOC is an active object that creates its own thread of execution during construction; such behavior would make for Hard-to-Test Code (page 209), and we should probably consider turning it into a Humble Executable (see Humble Object on page 695). If we have a large number of dependencies as constructor arguments, we probably need to refactor the code to remove this code smell.

变体:Setter 注入

与构造函数注入一样,SUT 持有对 DOC 的引用,作为在构造函数中初始化的属性(字段)。不同之处在于,该属性作为公共属性或通过“setter”方法暴露给客户端。当测试想要用测试替身替换真实 DOC 时,它会将测试替身的实例分配给暴露的属性(或调用 setter)。当构造真实 DOC 没有不良副作用并且假设在构造函数调用和测试调用属性的 setter 之间不会自动发生任何事情时,这种方法很有效。如果 SUT 在依赖于依赖关系的构造函数中执行任何重要处理,则不能使用Setter 注入。在这种情况下,我们必须使用构造函数注入。如果构造真实 DOC 有有害的副作用,我们可以通过修改 SUT 以使用延迟初始化[SBPP]在 SUT 第一次需要使用 DOC 时实例化 DOC,从而避免通过构造函数创建它。

As with Constructor Injection, the SUT holds a reference to the DOC as an attribute (field) that is initialized in the constructor. Where it differs is that the attribute is exposed to the client either as a public attribute or via a "setter" method. When a test wants to replace the real DOC with a Test Double, it assigns to the exposed attribute (or calls the setter with) an instance of the Test Double. This approach works well when constructing the real DOC has no unpleasant side effects and assuming that nothing can happen automatically between the constructor call and the point at which the test calls the setter for the property. Setter Injection cannot be used if the SUT performs any significant processing in the constructor that relies on the dependency. In that case, we must use Constructor Injection. If constructing the real DOC has deleterious side effects, we can avoid creating it via the constructor by modifying the SUT to use Lazy Initialization [SBPP] to instantiate the DOC the first time the SUT needs to use it.

改造依赖注入

当 SUT 不支持任何这些“现成”的选项时,我们可以通过测试特定子类来改进此功能。如果要使用的实际类通常是从配置数据中检索的,则此检索应由 SUT 以外的某个组件完成,然后使用依赖注入将该类传递给 SUT 。对客户端或配置使用Humble Object模式可将 SUT 与环境解耦,并确保测试不需要设置某些外部依赖项(配置文件)来引入测试替身

When the SUT does not support any of these options "out of the box," we may be able to retrofit this capability via a Test-Specific Subclass. If the actual class to be used is normally retrieved from configuration data, this retrieval should be done by some component other than the SUT and the class then passed to the SUT using Dependency Injection. Such a use of the Humble Object pattern for the client or configuration decouples the SUT from the environment and ensures that tests do not need to set up some external dependency (the configuration file) to introduce the Test Double.

另一种可能性是使用面向方面编程(AOP) 将依赖注入机制插入到开发环境中。例如,我们可以将使用测试替身的决定或将特定于测试的逻辑(测试替身)直接注入到 SUT 中。我认为我们在使用 AOP 方面的经验还不足以将其称为模式。

Another possibility is to use aspect-oriented programming (AOP) to insert the Dependency Injection mechanism into the development environment. For example, we might inject the decision to use the Test Double or inject the test-specific logic—the Test Double—directly into the SUT. I don't think we have enough experience with using AOP to call this a pattern just yet.

激励人心的例子

Motivating Example

以下测试无法“按原样”通过:

The following test cannot be made to pass "as is":

public void testDisplayCurrentTime_AtMidnight() {

      // 固定设置

      TimeDisplay sut = new TimeDisplay();

      // 练习 SUT

      String result = sut.getCurrentTimeAsHtmlFragment();

      // 验证直接输出

      String expectedTimeString =

               "<span class=\"tinyBoldText\">Midnight</span>";

      assertEquals( expectedTimeString, result);

}

public  void  testDisplayCurrentTime_AtMidnight()  {

      //  fixture  setup

      TimeDisplay  sut  =  new  TimeDisplay();

      //  exercise  SUT

      String  result  =  sut.getCurrentTimeAsHtmlFragment();

      //  verify  direct  output

      String  expectedTimeString  =

               "<span  class=\"tinyBoldText\">Midnight</span>";

      assertEquals(  expectedTimeString,  result);

}

 

此测试几乎总是会失败,因为它取决于 DOC 返回给 SUT 的当前时间。测试无法控制该组件返回的值DefaultTimeProvider。因此,只有当系统时间恰好为午夜时分,此测试才会通过。

This test almost always fails because it depends on the current time being returned to the SUT by a DOC. The test cannot control the values being returned by that component, the DefaultTimeProvider. Therefore, this test will pass only when the system time is exactly midnight.

公共字符串 getCurrentTimeAsHtmlFragment() {

      日历 currentTime;

      尝试 {

            currentTime = new DefaultTimeProvider().getTime();

      } catch (异常 e) {

            返回 e.getMessage();

      }

      // 等等

}

public  String  getCurrentTimeAsHtmlFragment()  {

      Calendar  currentTime;

      try  {

            currentTime  =  new  DefaultTimeProvider().getTime();

      }  catch  (Exception  e)  {

            return  e.getMessage();

      }

      //  etc.

}

 

由于 SUT 被硬编码为使用特定类来检索时间,因此我们无法用测试替身替换 DOC 。该限制使此测试不确定且几乎无用。我们需要找到一种方法来控制 SUT 的间接输入。

Because the SUT is hard-coded to use a particular class to retrieve the time, we cannot replace the DOC with a Test Double. That constraint makes this test nondeterministic and pretty much useless. We need to find a way to gain control over the indirect inputs of the SUT.

重构说明

Refactoring Notes

我们可以使用“用测试替身替换依赖项” (第 522页) 重构来控制时间。如果我们可以控制代码,并且相关方法使用不广泛,或者我们拥有支持引入参数[JBrains]重构的重构工具,则可以将 Setter 注入引入现有代码。如果做不到这一点,我们可以使用提取方法 [Fowler] 重构来创建以依赖注入为参数的新方法签名,并将旧方法保留为调用新方法的适配器[GOF]

We can use a Replace Dependency with Test Double (page 522) refactoring to gain control over the time. Setter Injection can be introduced into existing code if we have control over the code and the method in question is not widely used or if we have refactoring tools that support the Introduce Parameter [JBrains] refactoring. Failing that, we can use an Extract Method [Fowler] refactoring to create the new method signature that takes the Dependency Injection as an argument and leave the old method as an Adapter [GOF] that calls the new method.

示例:参数注入

Example: Parameter Injection

以下是使用参数注入重写的测试:

Here's the test rewritten to use Parameter Injection:

public void testDisplayCurrentTime_AtMidnight_PI() {

      // 夹具设置

      // 测试替身实例化

      TimeProvider tpStub = new MidnightTimeProvider();

      // 实例化 SUT

      TimeDisplay sut = new TimeDisplay();

      // 使用测试替身练习 SUT

      String result = sut.getCurrentTimeAsHtmlFragment(tpStub);

      // 验证结果

      String expectedTimeString =

               "<span class=\"tinyBoldText\">Midnight</span>";

      assertEquals("Midnight", expectedTimeString, result);

}

public  void  testDisplayCurrentTime_AtMidnight_PI()  {

      //  Fixture  setup

      //            Test  Double  instantiation

      TimeProvider  tpStub  =  new  MidnightTimeProvider();

      //            Instantiate  SUT

      TimeDisplay  sut  =  new  TimeDisplay();

      //  Exercise  SUT  using  Test  Double

      String  result  =  sut.getCurrentTimeAsHtmlFragment(tpStub);

      //  Verify  outcome

      String  expectedTimeString  =

               "<span  class=\"tinyBoldText\">Midnight</span>";

      assertEquals("Midnight",  expectedTimeString,  result);

}

 

在这种情况下,只有测试会使用新签名。现有代码可以使用旧签名,方法适配器会在传入真正的依赖对象之前先实例化它。

In this case, only the test will use the new signature. The existing code can use the old signature and the method adapter instantiates the real dependency object before passing it in.

公共字符串 getCurrentTimeAsHtmlFragment(

                              TimeProvider timeProviderArg){

      日历 currentTime;

      尝试{

            currentTime = timeProviderArg.getTime();

      } catch(异常e){

            返回e.getMessage();

      }

      //等

}

public  String  getCurrentTimeAsHtmlFragment(

                              TimeProvider  timeProviderArg)  {

      Calendar  currentTime;

      try  {

            currentTime  =  timeProviderArg.getTime();

      }  catch  (Exception  e)  {

            return  e.getMessage();

      }

      //  etc.

}

 

示例:构造函数注入

Example: Constructor Injection

以下是使用构造函数注入重写的相同测试:

Here's the same test rewritten to use Constructor Injection:

public void testDisplayCurrentTime_AtMidnight_CI()

                    throws Exception {

      // Fixture 设置

      // 测试替身实例化

      TimeProvider tpStub = new MidnightTimeProvider();

      // 实例化 SUT 注入测试替身

      TimeDisplay sut = new TimeDisplay(tpStub);

      // 练习 SUT

      String expectedTimeString =

               "<span class=\"tinyBoldText\">12:01 AM</span>";

      // 验证结果

            assertEquals("12:01 AM",

                                 expectedTimeString,

                                 sut.getCurrentTimeAsHtmlFragment());

}

public  void  testDisplayCurrentTime_AtMidnight_CI()

                    throws  Exception  {

      //  Fixture  setup

      //         Test  Double  instantiation

      TimeProvider  tpStub  =  new  MidnightTimeProvider();

      //      Instantiate  SUT  injecting  Test  Double

      TimeDisplay  sut  =  new  TimeDisplay(tpStub);

      //  Exercise  SUT

      String  expectedTimeString  =

               "<span  class=\"tinyBoldText\">12:01  AM</span>";

      //  Verify  outcome

            assertEquals("12:01  AM",

                                 expectedTimeString,

                                 sut.getCurrentTimeAsHtmlFragment());

}

 

要将 SUT 转换为使用构造函数注入,我们可以进行引入字段 [JetBrains] 重构,将 DOC 保存在现有构造函数中初始化的字段中。然后,我们可以进行引入参数重构,修改现有构造函数的所有调用者,以便它们将实际 DOC 作为构造函数的参数传递。如果我们无法或不想修改构造函数的所有现有调用者,我们可以定义一个以 DOC 作为参数的新构造函数,并修改现有构造函数以实例化实际 DOC 并将其传递给我们的新构造函数。

To convert the SUT to use Constructor Injection, we can do an Introduce Field [JetBrains] refactoring to hold the DOC in a field that is initialized in the existing constructor. We can then do an Introduce Parameter refactoring to modify all callers of the existing constructor so that they pass the real DOC as a parameter of the constructor. If we cannot or do not want to modify all existing callers of the constructor, we can define a new constructor that takes the DOC as a parameter and modify the existing constructor to instantiate the real DOC and pass it in to our new constructor.

public class TimeDisplay {



      private TimeProvider timeProvider;



      public TimeDisplay() { // 向后兼容的构造函数

            timeProvider = new DefaultTimeProvider();

      }

      public TimeDisplay(TimeProvider timeProvider) { // 新构造函数

            this.timeProvider = timeProvider;

      }

public  class  TimeDisplay  {



      private  TimeProvider  timeProvider;



      public  TimeDisplay()  {          //  backwards  compatible  constructor

            timeProvider  =  new  DefaultTimeProvider();

      }

      public  TimeDisplay(TimeProvider  timeProvider)  {  //  new  constructor

            this.timeProvider  =  timeProvider;

      }

 

另一种方法是对构造函数调用进行提取方法重构,然后使用移动方法 [Fowler] 重构将其移动到对象工厂(参见依赖项查找)。这将导致依赖项查找

Another approach is to do an Extract Method refactoring on the call to the constructor and then use Move Method [Fowler] refactoring to move it to an Object Factory (see Dependency Lookup). That would result in Dependency Lookup.

示例:Setter 注入

Example: Setter Injection

以下是使用Setter 注入重构的相同测试:

Here is the same test refactored to use Setter Injection:

public void testDisplayCurrentTime_AtMidnight_SI()

                    throws Exception {

      // Fixture 设置

      // 测试替身实例化

      TimeProvider tpStub = new MidnightTimeProvider();

      // 实例化 SUT

      TimeDisplay sut = new TimeDisplay();

      // 测试替身安装

      sut.setTimeProvider(tpStub);

      // 练习 SUT

      String result = sut.getCurrentTimeAsHtmlFragment();

      // 验证结果

      String expectedTimeString =

                   "<span class=\"tinyBoldText\">Midnight</span>";

      assertEquals("Midnight", expectedTimeString, result);

}

public  void  testDisplayCurrentTime_AtMidnight_SI()

                    throws  Exception  {

      //  Fixture  setup

      //            Test  Double  instantiation

      TimeProvider  tpStub  =  new  MidnightTimeProvider();

      //      Instantiate  SUT

      TimeDisplay  sut  =  new  TimeDisplay();

      //            Test  Double  installation

      sut.setTimeProvider(tpStub);

      //  Exercise  SUT

      String  result  =  sut.getCurrentTimeAsHtmlFragment();

      //  Verify  outcome

      String  expectedTimeString  =

                   "<span  class=\"tinyBoldText\">Midnight</span>";

      assertEquals("Midnight",  expectedTimeString,  result);

}

 

setTimeProvider注意安装硬编码测试替身(第 568页)的调用。如果我们使用了可配置测试替身(第 558页),其配置将在调用之前立即发生setTimeProvider

Note the call to setTimeProvider to install the Hard-Coded Test Double (page 568). If we had used a Configurable Test Double (page 558), its configuration would occur immediately before the call to setTimeProvider.

为了重构 SUT 以支持Setter 注入,我们可以进行引入字段重构,将 DOC 保存在现有构造函数中初始化的变量中,并通过此字段调用 DOC。然后,我们可以直接或通过 setter 公开该字段,以便测试可以覆盖其值。以下是重构后的 SUT 版本:

To refactor the SUT to support Setter Injection, we can do an Introduce Field refactoring to hold the DOC in a variable that is initialized in the existing constructor and call the DOC via this field. We can then expose the field either directly or via a setter so that the test can override its value. Here is the refactored version of the SUT:

公共类 TimeDisplay {



      私有 TimeProvider timeProvider;



      公共 TimeDisplay() {

            timeProvider = new DefaultTimeProvider();

      }

      公共 void setTimeProvider(TimeProvider provider) {

            this.timeProvider = provider;

      }

      公共 String getCurrentTimeAsHtmlFragment()

                 抛出 TimeProviderEx {

            日历 currentTime;

            尝试 {

                 currentTime = getTimeProvider().getTime();

            } catch (Exception e) {

                 return e.getMessage();

            }

            // 等等。

public  class  TimeDisplay  {



      private  TimeProvider  timeProvider;



      public  TimeDisplay()  {

            timeProvider  =  new  DefaultTimeProvider();

      }

      public  void  setTimeProvider(TimeProvider  provider)  {

            this.timeProvider  =  provider;

      }

      public  String  getCurrentTimeAsHtmlFragment()

                 throws  TimeProviderEx  {

            Calendar  currentTime;

            try  {

                 currentTime  =  getTimeProvider().getTime();

            }  catch  (Exception  e)  {

                 return  e.getMessage();

            }

            //  etc.

 

这里我们选择使用 getter 来检索 DOC。我们也可以很容易地timeProvider直接使用该字段。

Here we chose to use a getter to retrieve the DOC. We could just as easily have used the timeProvider field directly.

依赖项查找

Dependency Lookup

我们如何设计 SUT 以便我们可以在运行时替换它的依赖项?

How do we design the SUT so that we can replace its dependencies at runtime?

SUT 在使用另一个对象之前要求它返回所依赖的对象。

The SUT asks another object to return the depended-on object before it uses it.

也称为

Also known as

服务定位器、对象工厂、组件代理、组件注册表

Service Locator, Object Factory, Component Broker, Component Registry

图像

几乎每一段代码都依赖于其他一些类、对象、模块或过程。为了正确地对一段代码进行单元测试,我们希望将其与依赖项隔离开来。但是,如果这些依赖项以文字类名的形式硬编码在代码中,则很难实现这种隔离。

Almost every piece of code depends on some other classes, objects, modules, or procedures. To unit-test a piece of code properly, we would like to isolate it from its dependencies. Such isolation is difficult to achieve, however, if those dependencies are hard-coded within the code in the form of literal classnames.

依赖查找是一种允许在自动化测试期间打破 SUT 与其依赖项之间的正常耦合的方法。

Dependency Lookup is a way to allow the normal coupling between a SUT and its dependencies to be broken during automated testing.

工作原理

How It Works

我们避免将 SUT 所依赖的类的名称硬编码到我们的代码中,因为静态绑定严重限制了我们在软件运行时如何配置的选项。相反,我们将返回随时可用的对象的“组件代理”的名称硬编码。组件代理为客户端软件或系统配置管理器提供了一些方法来告诉相关 SUT 针对每个组件请求使用哪些对象。

We avoid hard-coding the names of classes on which the SUT depends into our code because static binding severely limits our options regarding how the software is configured as it runs. Instead, we hard-code that name of a "component broker" that returns a ready-to-use object. The component broker provides some means for the client software or perhaps a system configuration manager to tell the SUT in question which objects to use for each component request.

何时使用它

When to Use It

当我们需要从系统深处检索 DOC 时,依赖项查找是最合适的,而从客户端传入测试替身(第 522虚假数据库(参见551页的虚假对象) 或内存数据库(参见虚假对象) 替换系统的数据访问层,以加快自动客户测试的执行速度。对于每个皮下测试(参见第 337页的层测试) 来说,将虚假数据库通过服务门面[CJ2EEP]一直传递到数据访问层会过于复杂。使用依赖项查找允许测试甚至安装装饰器(第 447) 使用“配置门面”来安装虚假数据库,SUT 可以神奇地使用它而无需进一步处理。Jeremy Miller 写道:

Dependency Lookup is most appropriate when we need to retrieve DOCs from deep inside the system and it would be too messy to pass the Test Double (page 522) in from the client. A good example of such a situation is when we want to replace the data access layer of the system with a Fake Database (see Fake Object on page 551) or In-Memory Database (see Fake Object) to speed up execution of the automated customer tests. It would be too complex for each Subcutaneous Test (see Layer Test on page 337) to pass the Fake Database in through the Service Facade [CJ2EEP] and all the way down to the data access layer. Using Dependency Lookup allows the test or even a Setup Decorator (page 447) to use a "configuration facade" to install the Fake Database, which the SUT can magically use without any further ado. Jeremy Miller writes:

你不能低估使用服务定位器进行自动化测试的价值。我们经常在测试中使用替代依赖项,既是为了处理困难的依赖项,也是为了测试性能。例如,在功能测试中,我们会将网站和支持应用程序服务器合并为一个进程,以获得更好的性能。

You cannot understate the value of using a Service Locator for automated testing. We routinely use alternative dependencies in testing, both to deal with difficult dependencies and for test performance. For example, in a functional test we'll collapse a Web site and a backing application server into a single process for better performance.

 

依赖查找往往更容易被改造到现有的遗留软件上,因为它只影响对象构造实际发生的地方;我们不需要修改每个中间对象或方法,因为我们可能需要使用依赖注入第 678)。改造现有的往返测试也更简单,这样它们就可以使用伪对象,通过将它们包装在安装装饰器中来加快速度使用这种方案,我们不必更改每个测试;相反,我们可以在每个测试中创建 SUT 的新实例,并且仍然让测试使用相同的伪对象,因为服务定位器会在测试中记住它。2

Dependency Lookup tends to be a lot simpler to retrofit onto existing legacy software because it affects only those places where object construction actually occurs; we do not need to modify every intermediate object or method, as we might have to do with Dependency Injection (page 678). It is also much simpler to retrofit existing round-trip tests so that they use a Fake Object to speed them up by wrapping them in a Setup Decorator. With this scheme, we do not have to change each test; instead, we can create new instances of the SUT in each test and still have the test use the same Fake Object because the Service Locator remembers it across tests.2

依赖查找的主要替代方法是使用依赖注入在 SUT 中提供替换机制。这种方法通常更适合单元测试,因为它使 DOC 的替换更加明显,并且与 SUT 的执行直接相关。另一种选择是使用 AOP 使用开发工具安装特定于测试的逻辑,而不是修改软件的设计。最不受欢迎的解决方案是在 SUT 中使用测试钩子第 709页)以避免调用 DOC 或在 DOC 内使其以特定于测试的方式运行。

The main alternative to Dependency Lookup is to provide a substitution mechanism within the SUT using Dependency Injection. This approach is generally preferable for unit tests because it makes the replacement of the DOC more obvious and directly connected to exercising the SUT. Another option is to use AOP to install test-specific logic using the development tools rather than modifying the design of the software. The least preferred solution is to use a Test Hook (page 709) within the SUT to avoid calling the DOC or within the DOC so that it behaves in a test-specific way.

众所周知的中介可能被称为“服务定位器”、“对象工厂”、“组件代理”或“组件注册表”。虽然这些名称暗示了不同的语义(新对象与现有对象),但情况并非如此。出于性能原因,我们可以选择从“服务定位器”返回新对象,或从对象工厂返回“以前使用过的”对象。为了简化讨论,这里使用术语“组件代理”。

The well-known intermediary may be called a "Service Locator," "Object Factory," "Component Broker," or "Component Registry." While these names imply different semantics (new versus existing objects), this need not be the case. For performance reasons, we may choose to return new objects from a "Service Locator" or "previously enjoyed" objects from an Object Factory. To simplify this discussion, the term "Component Broker" is used here.

实施说明

Implementation Notes

在测试代​​码时使用测试替身意味着需要使 DOC 可替代。此约束排除了将我们所依赖的类的名称硬编码到代码中的可能性,因为静态绑定严重限制了我们在软件运行时配置方式的选择。避免此问题的一种方法是让 SUT 将 DOC 制作委托给另一个对象。当然,这种方案意味着我们需要一种方法来获取对该对象的引用。我们通过让一个众所周知的对象充当测试和 DOC 之间的中介来解决这个递归问题。这个众所周知的对象由硬编码的类名引用。为了便于安装测试替身,这个众所周知的对象必须提供一种机制,测试可以通过该机制指定要返回的对象。

A desire to use a Test Double when testing our code implies a need to make DOCs substitutable. This constraint rules out hard-coding the names of classes on which we depend into our code because static binding severely limits our options regarding how the software is configured as it runs. One way to avoid this issue is to have the SUT delegate DOC fabrication to another object. Of course, this scheme implies we need a way to get a reference to that object. We solve this recursive problem by having a well-known object act as an intermediary between the test and the DOC. This well-known object is referenced by a hard-coded classname. To be useful for installing Test Doubles, this well-known object must supply a mechanism by which the test can specify the object to be returned.

Dependency Lookup具有以下特点:

Dependency Lookup has the following characteristics:

  • 要么是单例(Singleton )[GOF],要么是注册表(Registry)[PEAA],要么是某种线程特定存储(Thread-Specific Storage )[POSA2]
  • Either a Singleton [GOF], a Registry [PEAA], or some kind of Thread-Specific Storage [POSA2]
  • 完全封装我们正在使用的实现的接口
  • An interface that fully encapsulates which implementation we are using
  • 内置替换机制,用于用测试替身替换返回的对象
  • A built-in substitution mechanism for replacing the returned object with a Test Double
  • 通过知名的全局名称访问
  • Access via well-known global name

依赖项查找机制返回一个可由客户端直接使用的对象。返回的实际对象的性质决定了将其称为“服务定位器”还是“对象工厂”更合适。检索到对象后,SUT 将直接使用它。在测试期间,测试安排依赖项查找机制返回特定于测试的对象。

The Dependency Lookup mechanism returns an object that can be used directly by the client. The nature of the actual object returned determines whether it is more appropriate to call it a "Service Locator" or an "Object Factory." Once the object is retrieved, the SUT uses it directly. During testing, the test arranges for the Dependency Lookup mechanism to return a test-specific object.

封装实现

依赖项查找的一个主要要求是存在一个众所周知的对象,我们可以将对 DOC 的请求委托给该对象。这个众所周知的对象可以是单例、注册表或某种特定于线程的存储机制。3

A major requirement of Dependency Lookup is the existence of a well-known object to which we can delegate our requests for DOCs. This well-known object could be a Singleton, a Registry, or some kind of Thread-Specific Storage mechanism.3

“组件代理”应从客户端(我们的 SUT)封装其实现。也就是说,“组件代理”提供的接口不应暴露它是单例还是注册表,或者是否在幕后使用某种类型的线程特定存储机制。事实上,测试环境可能希望提供不同的实现,专门用于避免测试中由单例引起的问题,例如可替换的单例(请参阅第579页的测试特定子类)。

The "Component Broker" should encapsulate its implementation from the client (our SUT). That is, the interface provided by the "Component Broker" should not expose whether it is a Singleton or a Registry or whether some type of Thread-Specific Storage mechanism is in use under the covers. In fact, the test environment may want to provide a different implementation specifically to avoid issues caused by Singletons in tests, such as a Substitutable Singleton (see Test-Specific Subclass on page 579).

替代机制

当测试想要用测试替身替换真实 DOC 时,它需要一种方法来告诉“组件代理”应该返回测试替身而不是真实组件。“组件代理”可以提供一个配置接口来为其配置要返回的对象,或者测试可以用合适的测试特定子类替换组件注册表。它可能还需要提供一种方法来恢复代理的原始或默认配置,以便一个测试中使用的配置不会“泄漏”到另一个测试中,从而有效地将“组件代理”更改为共享装置第 317页)。

When a test wants to replace the real DOC with a Test Double, it needs a way to tell the "Component Broker" that a Test Double should be returned instead of the real component. The "Component Broker" may provide a configuration interface to configure it with the object to be returned or the test can replace the component Registry with a suitable Test-Specific Subclass. It may also need to provide a way to restore the original or default configuration of the broker so that the configuration used in one test does not "leak" into another test, effectively changing the "Component Broker" into a Shared Fixture (page 317).

一个不太理想的配置替代方案是让“组件代理”从配置文件中读取要为每个请求构建的类名。但是,这种方法会带来几个问题。首先,除非测试提供了一种替代文件访问机制的方法,否则测试必须在夹具设置过程中写入文件。这肯定会导致测试速度变慢第 253页)。其次,除非配置文件也能为对象提供初始化数据,否则此方案不适用于可配置测试替身第 558页)。最后,需要写入文件为交互测试打开了大门(请参阅第 228页的不稳定测试),因为不同的测试可能需要不同的配置信息。

A less desirable configuration alternative is to have the "Component Broker" read the classnames to be constructed for each request from a configuration file. This approach poses several problems, however. First, the test must write the file as part of fixture setup unless the test offers a way to replace the file access mechanism. This is sure to result in Slow Tests (page 253). Second, this scheme will not work with Configurable Test Doubles (page 558) unless the configuration file can also provide initialization data for the object. Finally, the need to write a file opens the door to Interacting Tests (see Erratic Test on page 228) because different tests may need different configuration information.

如果“组件代理”必须根据配置数据返回对象,则更好的解决方案是让单独的Humble 对象第 695页)读取文件并调用“组件代理”上的配置接口。然后,测试可以使用相同的接口根据每个测试配置代理。

If the "Component Broker" must return objects based on configuration data, a better solution is to have a separate Humble Object (page 695) read the file and call a configuration interface on the "Component Broker." The test can then use this same interface to configure the broker on a per-test basis.

激励人心的例子

Motivating Example

以下测试无法“按原样”通过:

The following test cannot be made to pass "as is":

public void testDisplayCurrentTime_AtMidnight() {

      // 固定设置

      TimeDisplay sut = new TimeDisplay();

      // 练习 SUT

      String result = sut.getCurrentTimeAsHtmlFragment();

      // 验证直接输出

      String expectedTimeString =

               "<span class=\"tinyBoldText\">Midnight</span>";

      assertEquals( expectedTimeString, result);

}

public  void  testDisplayCurrentTime_AtMidnight()  {

      //  fixture  setup

      TimeDisplay  sut  =  new  TimeDisplay();

      //  exercise  SUT

      String  result  =  sut.getCurrentTimeAsHtmlFragment();

      //  verify  direct  output

      String  expectedTimeString  =

               "<span  class=\"tinyBoldText\">Midnight</span>";

      assertEquals(  expectedTimeString,  result);

}

 

此测试几乎总是会失败,因为它假设 DOC 将向 SUT 返回当前时间。DefaultTimeProvider但是,测试无法控制该组件 () 返回哪些值,因此只有当系统时间恰好为午夜时分,此测试才会通过。

This test almost always fails because it assumes that the current time will be returned to the SUT by a DOC. The test cannot control which values are returned by that component (the DefaultTimeProvider), however, so this test will pass only when the system time is exactly midnight.

公共字符串 getCurrentTimeAsHtmlFragment() {

      日历 currentTime;

      尝试 {

            currentTime = new DefaultTimeProvider().getTime();

      } catch (异常 e) {

            返回 e.getMessage();

      }

      // 等等

}

public  String  getCurrentTimeAsHtmlFragment()  {

      Calendar  currentTime;

      try  {

            currentTime  =  new  DefaultTimeProvider().getTime();

      }  catch  (Exception  e)  {

            return  e.getMessage();

      }

      //  etc.

}

 

由于 SUT 被硬编码为使用特定类来检索时间,因此我们无法用测试替身替换 DOC 。这使得该测试具有不确定性且几乎无用。我们需要找到一种方法来控制 SUT 的间接输入。

Because the SUT is hard-coded to use a particular class to retrieve the time, we cannot replace the DOC with a Test Double. That makes this test nondeterministic and pretty much useless. We need to find a way to gain control over the indirect inputs of the SUT.

重构说明

Refactoring Notes

使此行为可测试的第一步是用对“服务定位器”的调用替换硬编码的类名:

The first step to making this behavior testable is to replace the hard-coded classname with a call to a "Service Locator":

public String getCurrentTimeAsHtmlFragment() {

      日历 currentTime;

      尝试 {

            TimeProvider timeProvider =

                        (TimeProvider) ServiceLocator.getInstance().

                                                                          findService("Time");

            currentTime = timeProvider.getTime();

      } catch (Exception e) {

            return e.getMessage();

      }

      // 等等。

public  String  getCurrentTimeAsHtmlFragment()  {

      Calendar  currentTime;

      try  {

            TimeProvider  timeProvider  =

                        (TimeProvider)  ServiceLocator.getInstance().

                                                                          findService("Time");

            currentTime  =  timeProvider.getTime();

      }  catch  (Exception  e)  {

            return  e.getMessage();

      }

      //  etc.

 

尽管我们可以提供一个类方法来避免链式方法调用,但这一步只会将方法移到getInstance类方法中。下一个重构步骤取决于我们的“服务定位器”上是否有配置接口。如果配置“服务定位器”的生产版本有意义,我们可以将配置机制直接引入其中(如下一个示例所示)。否则,我们可以简单地在测试特定子类中覆盖服务定位器返回的内容(如第二个示例所示)。

Although we could have provided a class method to avoid the chained method calls, that step would just move the getInstance into the class method. The next refactoring step depends on whether we have a configuration interface on our "Service Locator." If it makes sense to configure the production version of the "Service Locator," we can introduce the configuration mechanism directly into it (as illustrated in the next example). Otherwise, we can simply override what the Service Locator returns in a Test-Specific Subclass (as illustrated in the second example).

示例:可配置注册表

Example: Configurable Registry

此版本的测试已被修改为使用“服务定位器”上的配置界面来安装测试替身:

This version of the test has been modified to use the configuration interface on the "Service Locator" to install a Test Double:

public void testDisplayCurrentTime_AtMidnight_CSL() {

      // 固定装置设置

      // 测试替身配置

      MidnightTimeProvider tpStub = new MidnightTimeProvider();

      // 实例化 SUT

      TimeDisplay sut = new TimeDisplay();

      // 测试替身安装

      ServiceLocator.getInstance().registerServiceForName(tpStub, "Time");

      // 练习 SUT

      String result = sut.getCurrentTimeAsHtmlFragment();

      // 验证结果

      String expectedTimeString =

                   "<span class=\"tinyBoldText\">Midnight</span>";

      assertEquals("Midnight", expectedTimeString, result);

}

public  void  testDisplayCurrentTime_AtMidnight_CSL()  {

      //  Fixture  setup

      //            Test  Double  configuration

      MidnightTimeProvider  tpStub  =  new  MidnightTimeProvider();

      //      Instantiate  SUT

      TimeDisplay  sut  =  new  TimeDisplay();

      //            Test  Double  installation

      ServiceLocator.getInstance().registerServiceForName(tpStub,  "Time");

      //  Exercise  SUT

      String  result  =  sut.getCurrentTimeAsHtmlFragment();

      //  Verify  outcome

      String  expectedTimeString  =

                   "<span  class=\"tinyBoldText\">Midnight</span>";

      assertEquals("Midnight",  expectedTimeString,  result);

}

 

SUT 中的代码之前已经描述过。可配置注册表的配置接口(参见可配置测试替身)的代码如下:

The code in the SUT was described previously. The code for the Configuration Interface (see Configurable Test Double) of the Configurable Registry follows:

public class ServiceLocator {

      protected ServiceLocator() {};



      protected static ServiceLocator soleInstance = null;



      public static ServiceLocator getInstance() {

            if (soleInstance==null)

               soleInstance = new ServiceLocator();

            return soleInstance;

      }



      private HashMap provider = new HashMap();



      public ServiceProvider findService(String serviceName) {

            return (ServiceProvider) provider.get(serviceName);

      }



      // 配置接口

      public void registerServiceForName( ServiceProvider provider,

                                                                  String serviceName) {

            provider.put( serviceName, provider);

      }

}

public  class  ServiceLocator  {

      protected  ServiceLocator()  {};



      protected  static  ServiceLocator  soleInstance  =  null;



      public  static  ServiceLocator  getInstance()  {

            if  (soleInstance==null)

               soleInstance  =  new  ServiceLocator();

            return  soleInstance;

      }



      private  HashMap  providers  =  new  HashMap();



      public  ServiceProvider  findService(String  serviceName)  {

            return  (ServiceProvider)  providers.get(serviceName);

      }



      //  configuration  interface

      public  void  registerServiceForName(  ServiceProvider  provider,

                                                                  String  serviceName)  {

            providers.put(  serviceName,  provider);

      }

}

 

这个例子的有趣之处在于我们在生产类上使用配置接口而不是测试替身。事实上,可配置注册表通过为测试提供一种机制来改变可配置注册表返回的服务组件,从而避免了使用测试替身的需要。

The interesting thing about this example is our use of a Configuration Interface on a production class rather than a Test Double. In fact, the Configurable Registry avoids the need to use a Test Double by providing the test with a mechanism to alter the service component the Configurable Registry returns.

示例:替代单例

Example: Substituted Singleton

此版本的测试通过将“服务定位器”的替换为替代单例(请参阅测试特定子类)来处理不可配置的依赖项查找机制。为了确保替代单例的配置接口的可重用性,我们将测试桩第 529页)作为参数传递给。soleInstanceTimeProvider overrideSoleInstance

This version of the test deals with a nonconfigurable Dependency Lookup mechanism by replacing the soleInstance of the "Service Locator" with a Substituted Singleton (see Test-Specific Subclass). To ensure the reusability of the configuration interface of the Substituted Singleton, we pass the TimeProvider Test Stub (page 529) as an argument to overrideSoleInstance.

public void testDisplayCurrentTime_AtMidnight_TSS() {

      // Fixture 设置

      // 测试替身配置

      MidnightTimeProvider tpStub = new MidnightTimeProvider();



      // 实例化 SUT

      TimeDisplay sut = new TimeDisplay();

      // 测试替身安装

      // 用一个

      始终返回测试桩的服务定位器替换整个服务定位器

      ServiceLocatorTestSingleton.overrideSoleInstance(tpStub);

      // 练习 SUT

      String result = sut.getCurrentTimeAsHtmlFragment();

      // 验证结果

      String expectedTimeString =

                   "<span class=\"tinyBoldText\">Midnight</span>";

      assertEquals("Midnight", expectedTimeString, result);

}

public  void  testDisplayCurrentTime_AtMidnight_TSS()  {

      //  Fixture  setup

      //            Test  Double  configuration

      MidnightTimeProvider  tpStub  =  new  MidnightTimeProvider();



      //      Instantiate  SUT

      TimeDisplay  sut  =  new  TimeDisplay();

      //            Test  Double  installation

      //              Replaces  the  entire  Service  Locator  with  one  that

      //              always  returns  our  Test  Stub

      ServiceLocatorTestSingleton.overrideSoleInstance(tpStub);

      //  Exercise  SUT

      String  result  =  sut.getCurrentTimeAsHtmlFragment();

      //  Verify  outcome

      String  expectedTimeString  =

                   "<span  class=\"tinyBoldText\">Midnight</span>";

      assertEquals("Midnight",  expectedTimeString,  result);

}

 

注意测试如何使用测试特定子类getInstance的实例覆盖通常返回的对象。单例代码如下:

Note how the test overrides the object normally returned by getInstance with an instance of a Test-Specific Subclass. The code for the Singleton follows:

公共类 ServiceLocator {

      protected ServiceLocator() {};



      protected static ServiceLocator soleInstance = null;

      公共静态 ServiceLocator getInstance() {

            if (soleInstance==null)

                soleInstance = new ServiceLocator();

            返回 soleInstance;

      }



      private HashMap provider = new HashMap();



      公共 ServiceProvider findService(String serviceName) {

            return (ServiceProvider) provider.get(serviceName);

      }

}

public  class  ServiceLocator  {

      protected  ServiceLocator()  {};



      protected  static  ServiceLocator  soleInstance  =  null;

      public  static  ServiceLocator  getInstance()  {

            if  (soleInstance==null)

                soleInstance  =  new  ServiceLocator();

            return  soleInstance;

      }



      private  HashMap  providers  =  new  HashMap();



      public  ServiceProvider  findService(String  serviceName)  {

            return  (ServiceProvider)  providers.get(serviceName);

      }

}

 

请注意,我们必须创建构造函数,而soleInstance  protected不是private允许子类重写它们。最后,这是替代单例的代码:

Note that we had to make the constructor and soleInstance  protected rather than private to allow them to be overridden by the subclass. Finally, here is the code for the Substituted Singleton:

public class ServiceLocatorTestSingleton extends ServiceLocator {

      private ServiceProvider tpStub;



      private ServiceLocatorTestSingleton(TimeProvider newTpStub) {

            this.tpStub = newTpStub;

      };



      // 安装接口

      static ServiceLocatorTestSingleton

                                   overrideSoleInstance(TimeProvider tpStub) {

            // 我们可以在重新分配 // soleInstance 之前保存真实实例

            ,以便稍后恢复它,但

            在本例中我们将放弃这种复杂性

            soleInstance = new ServiceLocatorTestSingleton( tpStub);

            return (ServiceLocatorTestSingleton) soleInstance;

      }



      // 重写的超类方法

      public ServiceProvider findService(String serviceName) {

            return tpStub; // 硬编码;忽略 serviceName

      }

}

public  class  ServiceLocatorTestSingleton  extends  ServiceLocator  {

      private  ServiceProvider  tpStub;



      private  ServiceLocatorTestSingleton(TimeProvider  newTpStub)  {

            this.tpStub  =  newTpStub;

      };



      //  Installation  interface

      static  ServiceLocatorTestSingleton

                                   overrideSoleInstance(TimeProvider  tpStub)  {

            //  We  could  save  the  real  instance  before  reassigning

            //  soleInstance  so  we  could  restore  it  later,  but  we'll

            //  forego  that  complexity  for  this  example

            soleInstance  =  new  ServiceLocatorTestSingleton(  tpStub);

            return  (ServiceLocatorTestSingleton)  soleInstance;

      }



      //  Overridden  superclass  method

      public  ServiceProvider  findService(String  serviceName)  {

            return  tpStub;    //  Hard-coded;  ignores  serviceName

      }

}

 

因为它看不到提供程序的私有性,所以此代码仅返回它在构造函数中初始化的字段HashMap的内容。tpStub

Because it cannot see the private HashMap of providers, this code simply returns the contents of the tpStub field that it initialized in the constructor.

关于名称

About the Name

为该模式选择一个名称非常困难。服务定位器组件代理已经广泛使用。这两个名称都非常适合在特定情况下使用。不幸的是,这两个名称都不能涵盖另一个名称,因此我不得不想出另一个名称来统一这两个主要变体。依赖注入这个名称已经广泛用于替代模式;出于对该名称一致性的期望,我们使用了依赖查找。有关此决策过程的更多信息,请参阅第576页的侧栏“ (模式)名称中包含什么? ” 。

Choosing a name for this pattern was tough. Service Locator and Component Broker were already in widespread use. Both are good names for use in their particular circumstance. Unfortunately, neither name can encompass the other, so I had to come up with another name that unified the two major variants. The name Dependency Injection was already in widespread use for the alternate pattern; a desire for consistency with that name led to using Dependency Lookup. See the sidebar "What's in a (Pattern) Name?" on page 576 for more on this decision-making process.

卑微的物体

Humble Object

当代码与其环境耦合过于紧密时,我们如何使代码可测试?

How can we make code testable when it is too closely coupled to its environment?

我们将逻辑提取到一个与其环境分离的、单独的、易于测试的组件中。

We extract the logic into a separate, easy-to-test component that is decoupled from its environment.

图像

我们经常需要尝试测试与某种框架紧密耦合的软件。例子包括可视化组件(例如,小部件、对话框)和事务组件插件。测试这些对象很困难,因为构造 SUT 需要与之交互的所有对象可能非常昂贵,甚至是不可能的。在其他情况下,对象可能难以测试,因为它们是异步运行的;例子包括活动对象(例如,线程、进程、Web 服务器)和用户界面。这些对象的异步性引入了不确定性、需要进程间协调以及需要延迟测试。面对这些棘手的问题,开发人员通常会放弃测试这类代码。结果:由未经测试的代码未经测试的需求导致的生产错误第 268页)。

We are often faced with trying to test software that is closely coupled to some kind of framework. Examples include visual components (e.g., widgets, dialogs) and transactional component plug-ins. Testing these objects is difficult because constructing all the objects with which our SUT needs to interact may be expensive—or even impossible. In other cases, objects may be hard to test because they run asynchronously; examples include active objects (e.g., threads, processes, Web servers) and user interfaces. These objects' asynchronicity introduces uncertainty, a requirement for interprocess coordination, and the need for delays into tests. Faced with these thorny issues, developers often just give up on testing this kind of code. The result: Production Bugs (page 268) caused by Untested Code and Untested Requirements.

Humble Object是一种以经济高效的方式对这些难以实例化的对象的逻辑进行测试的方法。

Humble Object is a way to bring the logic of these hard-to-instantiate objects under test in a cost-effective manner.

工作原理

How It Works

我们将难以测试的组件的所有逻辑提取到可通过同步测试测试的组件中。此组件实现一个服务接口,该接口由公开不可测试组件逻辑的方法组成;唯一的区别是这些方法可以通过同步方法调用访问。结果,Humble Object组件成为一个非常薄的适配器层,包含很少的代码。每次框架调用Humble Object时,此对象都会将其职责委托给可测试组件。如果可测试组件需要来自上下文的任何信息,Humble Object负责检索该信息并将其传递给可测试组件。Humble Object代码通常非常简单,我们常常不必为它编写测试,因为设置运行这些测试所需的环境可能非常困难。

We extract all the logic from the hard-to-test component into a component that is testable via synchronous tests. This component implements a service interface consisting of methods that expose the logic of the untestable component; the only difference is that these methods are accessible via synchronous method calls. As a result, the Humble Object component becomes a very thin adapter layer that contains very little code. Each time the framework calls the Humble Object, this object delegates its responsibilities to the testable component. If the testable component needs any information from the context, the Humble Object is responsible for retrieving it and passing it to the testable component. The Humble Object code is typically so simple that we often don't bother writing tests for it because it can be quite difficult to set up the environment needed to run those tests.

何时使用它

When to Use It

每当组件中存在一些非平凡逻辑时,我们就可以并且应该引入一个Humble Object ,因为该逻辑依赖于框架或只能异步访问,因此实例化会存在问题。对象难以测试的原因有很多;因此,打破所需依赖关系的方式有很多变化。以下变化是Humble Object最常见的示例— 但如果有时我们需要发明自己的变化,我们也不应该感到惊讶。

We can and should introduce a Humble Object whenever we have nontrivial logic in a component that is problematic to instantiate because it depends on a framework or can be accessed only asynchronously. There are lots of reasons for objects being hard to test; consequently, there are lots of variations in how we break the dependencies that are required. The following variations are the most common examples of Humble Object—but we shouldn't be surprised if we sometimes need to invent our own variation.

变体:谦逊对话

图形用户界面 (GUI) 框架要求我们提供对象来表示我们的页面和控件。这些对象提供逻辑来将用户操作转换为底层系统操作,并将系统响应转换回用户可识别的行为。此逻辑可能涉及调用用户界面背后的应用程序和/或修改此或其他可视对象的状态。

Graphical user interface (GUI) frameworks require us to provide objects to represent our pages and controls. These objects provide logic to translate user actions into the underlying system actions and to translate the system responses back into user recognizable behavior. This logic may involve invoking the application behind the user interface and/or modifying the state of this or other visual objects.

可视化对象很难进行有效测试,因为它们与调用它们的表示框架紧密耦合。 为了有效测试,测试需要模拟该环境,为可视化对象提供它所需的所有信息和功能。 使问题进一步复杂化的是,这些框架通常在它们自己的控制线程中运行,这意味着我们必须使用异步测试。 这些测试很难编写,并且经常导致测试缓慢(第253页)和不确定测试(请参阅第228页的不稳定测试)。 在这种情况下,我们可以使用谦逊对象将所有控制器和视图更新逻辑从依赖于框架的对象移到可测试对象中。

Visual objects are very difficult to test efficiently because they are tightly coupled to the presentation framework that invokes them. To be effective, a test would need to simulate that environment to provide the visual object with all the information and facilities it requires. Further complicating the issue is the fact that these frameworks often run in their own thread of control, which means that we must use asynchronous tests. These tests are challenging to write, and they often result in Slow Tests (page 253) and Nondeterministic Tests (see Erratic Test on page 228). Under these circumstances, we may benefit by using a Humble Object to move all of the controller and view-updating logic out of the framework-dependent object and into a testable object.

变体:Humble 可执行文件

许多程序都包含活动对象。活动对象有自己的执行线程,因此它们可以与系统的其他活动并行执行任务。活动对象的示例包括在单独进程(例如.exe文件中的 Windows 应用程序)或线程(在 Java 中,任何实现的对象Runnable)中运行的任何对象。这些对象可以由客户端直接启动,也可以自动启动,处理来自队列的请求并通过返回消息发送答复。无论哪种方式,我们都必须编写异步测试(包括进程间协调和/或显式延迟和Neverfail 测试;请参阅生产错误)来验证其行为。

Many programs contain active objects. Active objects have their own thread of execution so they can do things in parallel with other activities of the system. Examples of active objects include anything that runs in a separate process (e.g., Windows applications in .exe files) or thread (in Java, any object that implements Runnable). These objects may be launched directly by the client, or they may be started automatically, process requests from a queue, and send replies via a return message. Either way, we must write asynchronous tests (complete with interprocess coordination and/or explicit delays and Neverfail Tests; see Production Bugs) to verify their behavior.

低级可执行文件模式提供了一种方法,可以对可执行文件的逻辑进行测试,而不会产生可能导致慢速测试不确定测试的延迟。我们将可执行文件中的所有逻辑提取到可通过同步测试进行测试的组件中。该组件实现一个服务接口,该接口由公开可执行文件所有逻辑的方法组成;唯一的区别是这些方法可以通过同步方法调用访问。可测试组件可能是 Windows DLL、包含服务外观[CJ2EEP]类的 Java JAR,或者以可测试的方式公开可执行文件服务的其他语言组件或类。

The Humble Executable pattern provides a way to bring the logic of the executable under test without incurring the delays that might otherwise lead to Slow Tests and Nondeterministic Tests. We extract all the logic from the executable into a component that is testable via synchronous tests. This component implements a service interface consisting of methods that expose all logic of the executable; the only difference is that these methods are accessible via synchronous method calls. The testable component may be a Windows DLL, a Java JAR containing a Service Facade [CJ2EEP] class, or some other language component or class that exposes the services of the executable in a testable way.

Humble Executable组件本身包含的代码很少。它在其控制线程中所做的一切就是加载可测试组件(如果是True Humble Object)并委托给它。因此,Humble Executable只需要一两个测试来验证它是否正确执行了此加载/委托功能。尽管这些测试仍然需要几秒钟才能执行,但它们对整体测试套件执行时间的影响要小得多,因为它们的数量很少。鉴于此代码不会经常更改,甚至可以从开发人员在签入之前执行的测试套件中省略这些测试,以加快测试套件执行时间。当然,我们仍然希望将Humble Executable测试作为自动构建过程的一部分来运行。

The Humble Executable component itself contains very little code. All it does in its thread of control is to load the testable component (if a True Humble Object) and delegate to it. As a result, the Humble Executable requires only one or two tests to verify that it performs this load/delegate function correctly. Although these tests still take seconds to execute, they have a much smaller impact on the overall test suite execution time because so few of them exist. Given that this code will not change very often, these tests can even be omitted from the suite of tests that developers execute before check-in to speed up test suite execution times. Of course, we would still prefer to run the Humble Executable tests as part of the automated build process.

变体:Humble 事务控制器

许多应用程序使用数据库来保存其状态。使用数据库设置 Fixture 可能很慢且很复杂,而剩余的 Fixture 可能会对后续测试和测试运行造成严重破坏。如果我们使用共享 Fixture第 317页),则 Fixture 的持久性可能会导致不稳定的测试。Humble Transaction Controller通过使测试能够控制事务,方便测试在事务内运行的逻辑。因此,我们可以执行逻辑,验证结果,然后中止事务,而不会在数据库中留下任何活动痕迹。

Many applications use databases to persist their state. Fixture setup with databases can be slow and complex, and leftover fixtures can wreak havoc with subsequent tests and test runs. If we are using a Shared Fixture (page 317), the fixture's persistence may lead to Erratic Tests. Humble Transaction Controller facilitates testing of the logic that runs within the transaction by making it possible for the test to control the transaction. As a consequence, we can exercise the logic, verify the outcome, and then abort the transaction, leaving no trace of our activity in the database.

为了实现Humble Transaction Controller,我们使用 Extract Method [Fowler] 重构将我们要测试的所有逻辑从控制事务​​的代码中移出,移到一个单独的方法中,该方法对事务控制一无所知,并且可由测试调用。由于调用者控制事务,因此测试可以启动、提交(如果它选择这样做)和(最常见的)回滚事务。在这种情况下,行为(而不是依赖项)导致我们在测试业务逻辑时绕过Humble Object。因此,我们更有可能摆脱穷人的 Humble Object

To implement Humble Transaction Controller, we use an Extract Method [Fowler] refactoring to move all the logic we want to test out of the code that controls the transaction and into a separate method that knows nothing about transaction control and that can be called by the test. Because the caller controls the transaction, the test can start, commit (if it so chooses), and (most commonly) roll back the transaction. In this case, the behavior—not the dependencies—causes us to bypass the Humble Object when we are testing the business logic. As a result, we are more likely to be able to get away with a Poor Man's Humble Object.

至于Humble Object,它不包含任何业务逻辑。因此,唯一需要测试的行为是Humble Object是否根据其调用的方法的结果正确地提交和回滚事务。我们可以编写一个测试,用引发异常的测试桩(第 529页)替换可测试组件,然后验证此活动是否导致事务回滚。如果我们使用穷人的 Humble Object,则桩将实现为子类测试替身(请参阅第579页的测试特定子类),用引发异常的方法覆盖“真实”方法。

As for the Humble Object, it contains no business logic. Thus the only behavior that needs to be tested is whether the Humble Object commits and rolls back the transaction properly based on the outcome of the methods it calls. We can write a test that replaces the testable component with a Test Stub (page 529) that throws an exception and then verify that this activity results in a rollback of the transaction. If we are using a Poor Man's Humble Object, the stub would be implemented as a Subclassed Test Double (see Test-Specific Subclass on page 579) that overrides the "real" methods with methods that throw exceptions.

许多主流应用服务器技术都直接或间接地支持这种模式,它们将事务控制权从我们编写的业务对象中分离出来。如果我们在不使用事务控制框架的情况下构建软件,则可能需要实现自己的Humble Transaction Controller。请参阅“实现说明”部分,了解有关如何强制分离的一些想法。

Many of the major application server technologies support this pattern either directly or indirectly by taking transaction control away from the business objects that we write. If we are building our software without using a transaction control framework, we may need to implement our own Humble Transaction Controller. See the "Implementation Notes" section for some ideas on how we can enforce the separation.

变体:Humble 容器适配器

说到“容器”,我们经常必须实现特定的接口,以便我们的对象可以在应用服务器内运行(例如,“EJB 会话 bean”接口)。Humble Object模式的另一种变体是将我们的对象设计为独立于容器,然后让Humble Container Adapter将它们适配到容器所需的接口。这种策略使我们的逻辑组件易于在容器外进行测试,从而大大减少了“编辑-编译-测试”周期所需的时间。

Speaking of "containers," we often have to implement specific interfaces to allow our objects to run inside an application server (e.g., the "EJB session bean" interface). Another variation on the Humble Object pattern is to design our objects to be container-independent and then have a Humble Container Adapter adapt them to the interface required by container. This strategy makes our logic components easy to test outside the container, which dramatically reduces the time required for an "edit–compile–test" cycle.

实施说明

Implementation Notes

我们可以通过几种不同的方式使通常在Humble Object内部运行的逻辑可测试。所有这些技术都有一个共同点:它们都涉及公开逻辑,以便可以使用同步测试对其进行验证。然而,它们在逻辑公开方式方面有所不同。无论逻辑公开如何发生,测试驱动的纯粹主义者都希望测试验证Humble Object是否正确调用了提取的逻辑。这可以通过用某种测试替身(第 522页) 实现替换实际逻辑方法来实现。

We can make the logic that normally runs inside the Humble Object testable in several different ways. All of these techniques share one commonality: They involve exposing the logic so that it can be verified using synchronous tests. They vary, however, in terms of how the logic is exposed. Regardless of how logic exposure occurs, test-driven purists would prefer that tests verify that the Humble Object is calling the extracted logic properly. This can be done by replacing the real logic methods with some kind of Test Double (page 522) implementation.

变体:穷人的卑微之物

隔离和公开我们想要验证的每段逻辑的最简单方法是将其放入单独的方法中。我们可以通过在内联逻辑上使用提取方法重构来实现这一点,然后使结果方法在测试中可见。当然,此方法不能从上下文中获取任何内容。理想情况下,方法完成其工作所需的所有内容都将作为参数传入,但这些信息也可以放在字段中。如果可测试组件需要调用方法来访问所需的信息,并且这些方法依赖于(不存在/伪造的)上下文,则可能会出现问题,因为这种依赖性使编写测试更加复杂。

The simplest way to isolate and expose each piece of logic we want to verify is to place it into a separate method. We can do so by using an Extract Method refactoring on in-line logic and then making the resulting method visible from the test. Of course, this method cannot require anything from the context. Ideally everything the method needs to do its work will be passed in as arguments but this information could also be placed in fields. Problems may arise if the testable component needs to call methods to access information it needs and those methods are dependent on the (nonexistent/faked) context, as this dependency makes writing the tests more complex.

这种方法构成了“穷人的”谦卑对象,如果没有障碍阻止谦卑对象的实例化(例如,自动启动其线程、没有公共构造函数、无法满足的依赖关系),这种方法效果很好。使用特定于测试的子类也可以通过提供测试友好的构造函数并向测试公开私有方法来帮助打破这些依赖关系。

This approach, which constitutes the "poor man's" Humble Object, works well if no obstacles prevent the instantiation of the Humble Object (e.g., automatically starting its thread, no public constructor, unsatisfiable dependencies). Use of a Test-Specific Subclass can also help break these dependencies by providing a test-friendly constructor and exposing private methods to the test.

在测试子类化 Humble 对象穷人的 Humble 对象时,我们可以将测试间谍(第 538页) 构建为Humble 对象子类化测试替身,以记录相关方法的调用时间。然后,我们可以在测试方法(第 348页) 中使用断言来验证记录的值是否与预期值匹配。

When testing a Subclassed Humble Object or a Poor Man's Humble Object, we can build the Test Spy (page 538) as a Subclassed Test Double of the Humble Object to record when the methods in question were called. We can then use assertions within the Test Method (page 348) to verify that the values recorded match the values expected.

变体:真正的谦卑物体

在另一个极端,我们可以将要测试的逻辑放入一个单独的类中,并让Humble Object委托给它的一个实例。这种方法(在该模式的介绍中有所暗示)几乎适用于我们可以完全控制代码的任何情况。

At the other extreme, we can put the logic we want to test into a separate class and have the Humble Object delegate to an instance of it. This approach, which was implied in the introduction to this pattern, will work in almost any circumstance where we have complete control over the code.

有时,主机框架要求其对象承担某些我们无法移至其他地方的职责。例如,GUI 框架要求其视图对象包含 GUI 控件的数据以及这些控件在屏幕上显示的数据。在这些情况下,我们必须为可测试对象提供对Humble 对象的引用,并让其操作该对象的数据,或者在Humble 对象中放置一些最小的更新逻辑,并接受它不会被自动测试覆盖。前一种方法几乎总是可行的,而且总是更可取。

Sometimes the host framework requires that its objects hold certain responsibilities that we cannot move elsewhere. For example, a GUI framework expects its view objects to contain data for the controls of the GUI and the data that those controls display on the screen. In these cases we must either give the testable object a reference to the Humble Object and have it manipulate the data for that object or put some minimal update logic in the Humble Object and accept that it won't be covered by automated tests. The former approach is almost always possible and is always preferable.

要重构为真正的 Humble 对象,我们通常会进行一系列提取方法重构,以将Humble 对象的公共接口与我们计划委托的实现逻辑分离。然后,我们进行提取类 [Fowler] 重构,将所有方法(定义Humble 对象的公共接口的方法除外)移至新的“可测试”类。我们引入一个属性(字段)来保存对新类实例的引用,并将其初始化为新类的实例,作为构造函数的一部分或在每个接口方法中使用延迟初始化[SBPP] 。

To refactor to a True Humble Object, we normally do a series of Extract Method refactorings to decouple the public interface of the Humble Object from the implementation logic we plan to delegate. Then we do an Extract Class [Fowler] refactoring to move all the methods—except the ones that define the public interface of the Humble Object—to the new "testable" class. We introduce an attribute (a field) to hold a reference to an instance of the new class and initialize it to an instance of the new class either as part of the constructor or using Lazy Initialization [SBPP] in each interface method.

测试True Humble 对象(其中Humble 对象委托给单独的类)时,我们通常使用惰性模拟对象(请参阅第544页的模拟对象)或测试间谍来验证提取的类是否被正确调用。相比之下,在这种情况下使用更常见的活动模拟对象(请参阅模拟对象)是有问题的,因为断言是在与测试用例对象第 382页)不同的线程上进行的,除非我们找到一种方法将它们引导回测试线程,否则不会检测到故障。

When testing a True Humble Object (where the Humble Object delegates to a separate class), we typically use a Lazy Mock Object (see Mock Object on page 544) or Test Spy to verify that the extracted class is called correctly. By contrast, using the more common Active Mock Object (see Mock Object) is problematic in this situation because the assertions are made on a different thread from the Testcase Object (page 382) and failures won't be detected unless we find a way to channel them back to the test thread.

为了确保提取的可测试组件正确实例化,我们可以使用可观察对象工厂(请参阅第686页的依赖项查找)来构造提取的组件。测试可以注册为侦听器,以验证工厂上调用的方法是否正确。我们还可以使用常规工厂对象,并在测试期间将其替换为模拟对象测试桩,以监视调用了哪个工厂方法。

To ensure that the extracted testable component is instantiated properly, we can use an observable Object Factory (see Dependency Lookup on page 686) to construct the extracted component. The test can register as a listener to verify the correct method is called on the factory. We can also use a regular factory object and replace it during the test with a Mock Object or Test Stub to monitor which factory method was called.

变体:子类 Humble Object

在穷人的谦卑对象真正的谦卑对象这两个极端之间,存在着巧妙使用子类化的方法,将逻辑放入单独的类中,同时仍允许它们位于单个对象上。有多种不同的方法可以做到这一点,具体取决于谦卑对象类是否需要子类化特定的框架类。我不会在这里详细介绍,因为这种技术特定于语言和运行时环境。不过,您应该认识到,基本选项是让依赖框架的类从超类继承要测试的逻辑,或者让该类委托给由子类实现的抽象方法。

In between the extremes of the Poor Man's Humble Object and the True Humble Object are approaches that involve clever use of subclassing to put the logic into separate classes while still allowing them to be on a single object. A number of different ways to do this are possible, depending on whether the Humble Object class needs to subclass a specific framework class. I won't go into a lot of detail here as this technique is very specific to the language and runtime environment. Nevertheless, you should recognize that the basic options are either having the framework-dependent class inherit the logic to be tested from a superclass or having the class delegate to an abstract method that is implemented by a subclass.

激励示例(Humble 可执行文件)

Motivating Example (Humble Executable)

在此示例中,我们测试一些在其自己的线程中运行并处理每个请求的逻辑。在每个测试中,我们启动线程,向其发送一些消息,并等待足够长的时间以使我们的断言通过。不幸的是,线程需要几秒钟才能启动、初始化并处理第一个请求。因此,除非我们在启动线程后包含两秒钟的延迟,否则测试偶尔会失败。

In this example, we are testing some logic that runs in its own thread and processes each request as it arrives. In each test, we start up the thread, send it some messages, and wait long enough so that our assertions pass. Unfortunately, it takes several seconds for the thread to start up, become initialized, and process the first request. Thus the test fails sporadically unless we include a two-second delay after starting the thread.

public class RequestHandlerThreadTest extends TestCase {

      private static final int TWO_SECONDS = 3000;



      public void testWasInitialized_Async()

                        throws InterruptedException {

            // 设置

            RequestHandlerThread sut = new RequestHandlerThread();

            // 执行

            sut.start();

            // 验证

            Thread.sleep(TWO_SECONDS);

            assertTrue(sut.initializedSuccessfully());

      }



      public void testHandleOneRequest_Async()

                    throws InterruptedException {

            // 设置

            RequestHandlerThread sut = new RequestHandlerThread();

            sut.start();

            // 执行

            enqueRequest(makeSimpleRequest());

            // 验证

            Thread.sleep(TWO_SECONDS);

            assertEquals(1, sut.getNumberOfRequestsCompleted());

            assertResponseEquals(makeSimpleResponse(), getResponse());

      }

}

public  class  RequestHandlerThreadTest  extends  TestCase  {

      private  static  final  int  TWO_SECONDS  =  3000;



      public  void  testWasInitialized_Async()

                        throws  InterruptedException  {

            //  Setup

            RequestHandlerThread  sut  =  new  RequestHandlerThread();

            //  Exercise

            sut.start();

            //        Verify

            Thread.sleep(TWO_SECONDS);

            assertTrue(sut.initializedSuccessfully());

      }



      public  void  testHandleOneRequest_Async()

                    throws  InterruptedException  {

            //  Setup

            RequestHandlerThread  sut  =  new  RequestHandlerThread();

            sut.start();

            //  Exercise

            enqueRequest(makeSimpleRequest());

            //  Verify

            Thread.sleep(TWO_SECONDS);

            assertEquals(1,  sut.getNumberOfRequestsCompleted());

            assertResponseEquals(makeSimpleResponse(),  getResponse());

      }

}

 

理想情况下,我们希望分别测试每种事务的线程,以实现更好的缺陷定位(参见第 22页)。不幸的是,如果我们这样做,我们的测试套件将需要几分钟才能运行,因为每个测试都包括几秒钟的延迟。另一个问题是,如果我们的活动对象在其自己的线程中发生异常,测试将不会导致错误。

Ideally, we would like to test the thread with each kind of transaction individually to achieve better Defect Localization (see page 22). Unfortunately, if we did so our test suite would take many minutes to run because each test includes a delay of several seconds. Another problem is that the tests won't result in an error if our active object has an exception in its own thread.

两秒钟的延迟似乎不是什么大问题,但想想当我们有十几个这样的测试时会发生什么。运行这些测试需要近半分钟的时间。将这种性能与正常测试的性能进行对比——我们每秒可以运行数百个这样的测试。通过可执行文件进行测试会对我们的工作效率产生负面影响。为了记录,以下是可执行文件的代码:

A two-second delay may not seem like a big deal, but consider what happens when we have a dozen such tests. It would take us almost half a minute to run these tests. Contrast this performance with that of normal tests—we can run several hundred of those tests each second. Testing via the executable is affecting our productivity negatively. For the record, here's the code for the executable:

公共类 RequestHandlerThread 扩展了 Thread {

      私有布尔值 _initializationCompleted = false;

      私有 int _numberOfRequests = 0;



      公共 void run() {

            初始化线程();

            processRequestsForever();

      }



      公共布尔值 InitializedSuccessfully() {

            返回 _initializationCompleted;

      }



      void processRequestsForever() {

            请求请求 = nextMessage();

            执行 {

                  响应响应 = processOneRequest(请求);

                  如果 (响应 != null) {

                       putMsgOntoOutputQueue(响应);

                  }

                  请求 = nextMessage();

            } while (请求 != null);

      }

}

public  class  RequestHandlerThread  extends  Thread  {

      private  boolean  _initializationCompleted  =  false;

      private  int  _numberOfRequests  =  0;



      public  void  run()    {

            initializeThread();

            processRequestsForever();

      }



      public  boolean  initializedSuccessfully()  {

            return  _initializationCompleted;

      }



      void  processRequestsForever()  {

            Request  request  =  nextMessage();

            do  {

                  Response  response  =  processOneRequest(request);

                  if  (response  !=  null)  {

                       putMsgOntoOutputQueue(response);

                  }

                  request  =  nextMessage();

            }  while  (request  !=  null);

      }

}

 

为了避免业务逻辑的干扰,我已经使用了 Extract Method 重构将实际逻辑移至方法中processOneRequest。同样,实际的初始化逻辑未在此处显示;只需说明此逻辑_initializationCompleted在成功完成时设置变量即可。

To avoid the distraction of the business logic, I have already used an Extract Method refactoring to move the real logic into the method processOneRequest. Likewise, the actual initialization logic is not shown here; suffice it to say that this logic sets the variable _initializationCompleted when it finishes successfully.

重构说明

Refactoring Notes

要创建Poor Man's Humble Object,我们需要公开方法,使它们在测试中可见。(如果代码使用内联逻辑,我们将首先执行 Extract Method 重构。)如果上下文有任何依赖项,我们需要执行 Introduce Parameter [JBrains]重构或 Introduce Field [JetBrains] 重构,以便该processOneRequest方法不需要从上下文访问任何内容。

To create a Poor Man's Humble Object, we expose the methods to make them visible from the test. (If the code used in-line logic, we would do an Extract Method refactoring first.) If there were any dependencies on the context, we would need to do an Introduce Parameter [JBrains] refactoring or an Introduce Field [JetBrains] refactoring so that the processOneRequest method need not access anything from the context.

要创建真正的Humble Object,我们可以对可执行文件进行 Extract Class 重构,以创建可测试组件,只留下Humble Object作为空壳。此步骤通常涉及执行上述 Extract Method 重构,以将我们要测试的逻辑(例如initializeThread方法和processOneRequest方法)与与可执行文件上下文交互的逻辑分开。然后,我们进行 Extract Class 重构以引入可测试组件类(本质上是一个 Strategy [GOF]对象),并将除公共接口方法之外的所有方法移至该类。Extract Class 重构包括引入一个字段来保存对新对象的引用并创建一个实例。它还包括修复所有公共方法,以便它们调用移至新的可测试类的方法。

To create a true Humble Object, we can do an Extract Class refactoring on the executable to create the testable component, leaving behind just the Humble Object as an empty shell. This step typically involves doing the Extract Method refactoring described above to separate the logic we want to test (e.g., the initializeThread method and the processOneRequest method) from the logic that interacts with the context of the executable. We then do an Extract Class refactoring to introduce the testable component class (essentially a single Strategy [GOF] object) and move all methods except the public interface methods over to it. The Extract Class refactoring includes introducing a field to hold a reference to the new object and creating an instance. It also includes fixing all of the public methods so that they call the methods that were moved to the new testable class.

示例:穷人的卑微可执行文件

Example: Poor Man's Humble Executable

以下是用“穷人的卑微对象”重写的同一组测试

Here is the same set of tests rewritten as a Poor Man's Humble Object:

public void testWasInitialized_Sync()

              throws InterruptedException {

      // 设置

      RequestHandlerThread sut = new RequestHandlerThread();

      // 练习

      sut.initializeThread();

      // 验证

      assertTrue(sut.initializedSuccessfully());

}



public void testHandleOneRequest_Sync()

               throws InterruptedException {

      // 设置

      RequestHandlerThread sut = new RequestHandlerThread();

      // 练习

      Response response = sut.processOneRequest(makeSimpleRequest());

      // 验证

      assertEquals(1, sut.getNumberOfRequestsCompleted());

      assertResponseEquals(makeSimpleResponse(), response);

}

public  void  testWasInitialized_Sync()

              throws  InterruptedException  {

      //  Setup

      RequestHandlerThread  sut  =  new  RequestHandlerThread();

      //  Exercise

      sut.initializeThread();

      //  Verify

      assertTrue(sut.initializedSuccessfully());

}



public  void  testHandleOneRequest_Sync()

               throws  InterruptedException  {

      //  Setup

      RequestHandlerThread  sut  =  new  RequestHandlerThread();

      //  Exercise

      Response  response  =  sut.processOneRequest(makeSimpleRequest());

      //  Verify

      assertEquals(1,  sut.getNumberOfRequestsCompleted());

      assertResponseEquals(makeSimpleResponse(),  response);

}

 

在这里,我们将方法initializeThreadprocessOneRequest公开,以便我们可以从测试中同步调用它们。请注意,此测试中没有延迟。只要我们可以轻松实例化可执行组件,这种方法就很有效。

Here, we have made the methods initializeThread and processOneRequest public so that we can call them synchronously from the test. Note the absence of a delay in this test. This approach works well as long as we can instantiate the executable component easily.

示例:真正的 Humble 可执行文件

Example: True Humble Executable

以下是我们使用True Humble Executable重构后的 SUT 代码:

Here is the code for our SUT refactored to use a True Humble Executable:

公共类 HumbleRequestHandlerThread 扩展了 Thread

实现 Runnable {

      公共 RequestHandler requestHandler;



      公共 HumbleRequestHandlerThread() {

            super();

            requestHandler = new RequestHandlerImpl();

      }



      公共 void run() {

            requestHandler.initializeThread();

            processRequestsForever();

      }



      公共布尔初始化成功() {

            return requestHandler.initializedSuccessfully();

      }



      公共 void processRequestsForever() {

            请求请求 = nextMessage();

            执行 {

                  响应响应 = requestHandler.processOneRequest(request);

                  如果 (响应 != null) {

                        putMsgOntoOutputQueue(响应);

                  }

                  请求 = nextMessage();

              } while (请求 != null);

      }

public  class  HumbleRequestHandlerThread  extends  Thread

implements  Runnable  {

      public  RequestHandler  requestHandler;



      public  HumbleRequestHandlerThread()  {

            super();

            requestHandler  =  new  RequestHandlerImpl();

      }



      public  void  run()  {

            requestHandler.initializeThread();

            processRequestsForever();

      }



      public  boolean  initializedSuccessfully()  {

            return  requestHandler.initializedSuccessfully();

      }



      public  void  processRequestsForever()  {

            Request  request  =  nextMessage();

            do  {

                  Response  response  =  requestHandler.processOneRequest(request);

                  if  (response  !=  null)  {

                        putMsgOntoOutputQueue(response);

                  }

                  request  =  nextMessage();

              }  while  (request  !=  null);

      }

 

在这里,我们将方法移至processOneRequest一个单独的类,以便于实例化。下面是重写后的相同测试,以利用提取的组件。请注意,此测试中没有延迟。

Here, we have moved the method processOneRequest to a separate class that we can instantiate easily. Below is the same test rewritten to take advantage of the extracted component. Note the absence of a delay in this test.

public void testNotInitialized_Sync()

               throws InterruptedException {

      // 设置/执行

      RequestHandler sut = new RequestHandlerImpl();

      // 验证

      assertFalse("init", sut.initializedSuccessfully());

}



public void testWasInitialized_Sync()

               throws InterruptedException {

      // 设置

      RequestHandler sut = new RequestHandlerImpl();

      // 执行

      sut.initializeThread();

      // 验证

      assertTrue("init", sut.initializedSuccessfully());

}



public void testHandleOneRequest_Sync()

               throws InterruptedException {

      // 设置

      RequestHandler sut = new RequestHandlerImpl();

      // 执行

      Response response = sut.processOneRequest( makeSimpleRequest() );

      // 验证

      assertEquals( 1, sut.getNumberOfRequestsDone());

      assertResponseEquals( makeSimpleResponse(), response);

}

public  void  testNotInitialized_Sync()

               throws  InterruptedException  {

      //  Setup/Exercise

      RequestHandler  sut  =  new  RequestHandlerImpl();

      //  Verify

      assertFalse("init",  sut.initializedSuccessfully());

}



public  void  testWasInitialized_Sync()

               throws  InterruptedException  {

      //      Setup

      RequestHandler  sut  =  new  RequestHandlerImpl();

      //      Exercise

      sut.initializeThread();

      //  Verify

      assertTrue("init",  sut.initializedSuccessfully());

}



public  void  testHandleOneRequest_Sync()

               throws  InterruptedException  {

      //  Setup

      RequestHandler  sut  =  new  RequestHandlerImpl();

      //  Exercise

      Response  response  =  sut.processOneRequest(  makeSimpleRequest()  );

      //  Verify

      assertEquals(  1,  sut.getNumberOfRequestsDone());

      assertResponseEquals(  makeSimpleResponse(),  response);

}

 

因为我们已经将委托引入另一个对象,所以我们可能应该验证委托是否正确发生。下一个测试验证Humble 对象是否调用了新创建的可测试组件上的initializeThread方法和方法:processOneRequest

Because we have introduced delegation to another object, we should probably verify that the delegation occurs properly. The next test verifies that the Humble Object calls the initializeThread method and the processOneRequest method on the newly created testable component:

public void testLogicCalled_Sync()

                  throws InterruptedException {

      // 设置

      RequestHandlerRecordingStub mockHandler =

                  new RequestHandlerRecordingStub();

      HumbleRequestHandlerThread sut = new HumbleRequestHandlerThread();

      // 模拟安装

      sut.setHandler( mockHandler );

      sut.start();

      // 练习

      enqueRequest(makeSimpleRequest());

      // 验证

      Thread.sleep(TWO_SECONDS);

      assertTrue("init", mockHandler.initializedSuccessfully() );

      assertEquals( 1, mockHandler.getNumberOfRequestsDone() );

}

public  void  testLogicCalled_Sync()

                  throws  InterruptedException  {

      //  Setup

      RequestHandlerRecordingStub  mockHandler  =

                  new  RequestHandlerRecordingStub();

      HumbleRequestHandlerThread  sut  =  new  HumbleRequestHandlerThread();

      //        Mock  Installation

      sut.setHandler(  mockHandler  );

      sut.start();

      //  Exercise

      enqueRequest(makeSimpleRequest());

      //  Verify

      Thread.sleep(TWO_SECONDS);

      assertTrue("init",  mockHandler.initializedSuccessfully()  );

      assertEquals(  1,  mockHandler.getNumberOfRequestsDone()  );

}

 

请注意,此测试确实需要至少一小段延迟才能让线程启动。但是,延迟时间更短,因为我们已将实际逻辑组件替换为可立即响应的测试替身,现在只有一个测试需要延迟。我们甚至可以将此测试移至运行频率较低的单独测试套件(例如,仅在自动构建过程中运行),以确保每次签入之前执行的所有测试都能快速运行。

Note that this test does require at least a small delay to allow the thread to start up. The delay is shorter, however, because we have replaced the real logic component with a Test Double that responds instantly and only one test now requires the delay. We could even move this test to a separate test suite that is run less frequently (e.g., only during the automated build process) to ensure that all tests performed before each check-in run quickly.

需要注意的另一件重要事情是,我们使用的是测试间谍而不是模拟对象。由于模拟对象所做的断言将在与测试方法不同的线程中引发,因此测试自动化框架(第298页)— 在本例中为 JUnit — 不会捕获它们。因此,即使模拟对象中的断言失败,测试也可能显示“通过”。通过在测试方法中进行断言,我们避免了必须执行某些特殊操作来将模拟对象抛出的异常传递回测试方法正在执行的线程。

The other significant thing to note is that we are using a Test Spy rather than a Mock Object. Because the assertions done by the Mock Object would be raised in a different thread from the Test Method, the Test Automation Framework (page 298)—in this example, JUnit—won't catch them. As a consequence, the test might indicate "pass" even though assertions in the Mock Object are failing. By making the assertions in the Test Method, we avoid having to do something special to relay the exceptions thrown by the Mock Object back to the thread in which the Test Method is executing.

上述测试验证了我们的Humble 对象确实委托给了我们安装的Test Spy 。验证我们的Humble 对象确实将保存委托的变量初始化为适当的类也是一个好主意。以下是一个简单的方法:

The preceding test verified that our Humble Object actually delegates to the Test Spy that we have installed. It would also be a good idea to verify that our Humble Object actually initializes the variable holding the delegate to the appropriate class. Here's a simple way to do so:

public void testConstructor() {

      // 练习

      HumbleRequestHandlerThread sut = new HumbleRequestHandlerThread();

      // 验证

      字符串 actualDelegateClass = sut.requestHandler.getClass().getName();

      assertEquals( RequestHandlerImpl.class.getName(),

                            actualDelegateClass);

      }

public  void  testConstructor()  {

      //  Exercise

      HumbleRequestHandlerThread  sut  =  new  HumbleRequestHandlerThread();

      //  Verify

      String  actualDelegateClass  =  sut.requestHandler.getClass().getName();

      assertEquals(  RequestHandlerImpl.class.getName(),

                            actualDelegateClass);

      }

 

构造函数测试(参见测试方法)验证特定属性是否已初始化。

This Constructor Test (see Test Method) verifies that a specific attribute has been initialized.

示例:谦逊对话

Example: Humble Dialog

许多开发环境允许我们通过将各种对象(“小部件”)拖放到画布上来直观地构建用户界面。它们允许我们通过选择特定于该可视化对象的几个可能操作或事件之一并在 IDE 提供的代码窗口中输入逻辑来为这些可视化对象添加行为。此逻辑可能涉及调用用户界面背后的应用程序,也可能涉及修改此可视化对象或其他可视化对象的状态。

Many development environments let us build the user interface visually by dragging and dropping various objects ("widgets") onto a canvas. They let us add behavior to these visual objects by selecting one of several possible actions or events specific to that visual object and typing logic into the code window presented by the IDE. This logic may involve invoking the application behind the user interface or it may involve modifying the state of this or some other visual object.

可视化对象很难进行有效测试,因为它们与调用它们的表示框架紧密耦合。为了向可视化对象提供其所需的所有信息和功能,测试需要模拟该环境 — 这是一项相当大的挑战。这使得测试变得非常复杂,以至于许多开发团队根本不费心测试表示逻辑。毫不奇怪,这种缺乏测试的情况经常导致由未经测试的代码和未经测试的需求引起的生产错误

Visual objects are very difficult to test efficiently because they are tightly coupled to the presentation framework that invokes them. To provide the visual object with all the information and facilities it requires, the test would need to simulate that environment—quite a challenge. This makes testing very complicated, so much so that many development teams don't bother testing the presentation logic at all. This lack of testing, not surprisingly, often leads to Production Bugs caused by untested code and Untested Requirements.

要创建Humble Dialog,我们将视图组件中的所有逻辑提取到可通过同步测试进行测试的非可视组件中。如果此组件需要更新视图对象(Humble Dialog)的状态,则将Humble Dialog作为参数传入。在测试非可视组件时,我们通常将Humble Dialog替换为配置了间接输入值和预期行为(间接输出)的模拟对象。在要求Humble Dialog向框架注册其希望看到的每个事件的 GUI 框架中,非可视组件可以注册自身而不是Humble Dialog(只要这不会引入对上下文的难以管理的依赖)。这种灵活性使Humble Dialog更加简单,因为事件直接进入非可视组件并且不需要委托逻辑。

To create the Humble Dialog, we extract all the logic from the view component into a nonvisual component that is testable via synchronous tests. If this component needs to update the view object's (Humble Dialog's) state, the Humble Dialog is passed in as an argument. When testing the nonvisual component, we typically replace the Humble Dialog with a Mock Object that is configured with the indirect input values and the expected behavior (indirect outputs). In GUI frameworks that require the Humble Dialog to register itself with the framework for each event it wishes to see, the nonvisual component can register itself instead of the Humble Dialog (as long as that doesn't introduce unmanageable dependencies on the context). This flexibility makes the Humble Dialog even simpler because the events go directly to the nonvisual component and require no delegation logic.

以下代码示例取自.ctl包含一些重要逻辑的 VB 视图组件 ()。它是我们为 Mercury Interactive 的 TestDirector 工具构建的自定义插件的一部分。

The following code sample is taken from a VB view component (.ctl) that includes some nontrivial logic. It is part of a custom plug-in we built for Mercury Interactive's TestDirector tool.

  ' 接口方法,TestDirector 将调用此方法

  ' 显示结果。Public

Sub ShowResultEx(TestSetKey As TdTestSetKey, _

                                           TSTestKey As TdTestKey, _

                                           ResultKey As TdResultKey)

        Dim RpbFiles As OcsRpbFiles

        Set RpbFiles = getTestResultFileNames(ResultKey)

        ResultsFileName = RpbFiles.ActualResultFileName

        ShowFileInBrowser ResultsFileName

End Sub



Function getTestResultFileNames(ResultKey As Variant) As OcsRpbFiles

        On Error GoTo Error

        Dim Attachments As Collection

        Dim thisTest As Run

        Dim RpbFiles As New OcsRpbFiles



        Call EnsureConnectedToTd



        Set Attachments = testManager.GetAllAttachmentsOfRunTest(ResultKey)

        Call RpbFiles.LoadFromCollection(Attachments, "RunTest")

        Set getTestResultFileNames = RpbFiles

        Exit Function

Error:

        ' 执行某事...

End Function

  '  Interface  method,  TestDirector  will  call  this  method

  '  to  display  the  results.

Public  Sub  ShowResultEx(TestSetKey  As  TdTestSetKey,  _

                                           TSTestKey  As  TdTestKey,  _

                                           ResultKey  As  TdResultKey)

        Dim  RpbFiles  As  OcsRpbFiles

        Set  RpbFiles  =  getTestResultFileNames(ResultKey)

        ResultsFileName  =  RpbFiles.ActualResultFileName

        ShowFileInBrowser  ResultsFileName

End  Sub



Function  getTestResultFileNames(ResultKey  As  Variant)  As  OcsRpbFiles

        On  Error  GoTo  Error

        Dim  Attachments  As  Collection

        Dim  thisTest  As  Run

        Dim  RpbFiles  As  New  OcsRpbFiles



        Call  EnsureConnectedToTd



        Set  Attachments  =  testManager.GetAllAttachmentsOfRunTest(ResultKey)

        Call  RpbFiles.LoadFromCollection(Attachments,  "RunTest")

        Set  getTestResultFileNames  =  RpbFiles

        Exit  Function

Error:

        '  do  something  ...

End  Function

 

理想情况下,我们希望测试逻辑。不幸的是,我们无法构造作为参数传入的对象,因为它们没有公共构造函数。传入其他类型的对象也是不可能的,因为函数参数的类型被硬编码为特定的具体类。

Ideally, we would like to test the logic. Unfortunately, we cannot construct the objects passed in as parameters because they don't have public constructors. Passing in objects of some other type isn't possible either, because the types of the function parameters are hard-coded to be specific concrete classes.

我们可以对可执行文件进行提取可测试组件 (第 735页) 重构,以创建可测试组件,只留下Humble Dialog作为空壳。此方法通常涉及执行几个提取方法重构(已在原始示例中完成,以使重构更易于理解),每个要移动的逻辑块都进行一次重构。然后,我们执行提取类重构以创建新的可测试组件类。提取类重构可能包括移动方法 [Fowler] 和移动字段 [Fowler] 重构,以将逻辑及其所需的数据从Humble Dialog移出并移入新的可测试组件。

We can do an Extract Testable Component (page 735) refactoring on the executable to create the testable component, leaving behind just the Humble Dialog as an empty shell. This approach typically involves doing several Extract Method refactorings (already done in the original example to make the refactoring easier to understand), one for each chunk of logic that we want to move. We then do an Extract Class refactoring to create our new testable component class. The Extract Class refactoring may include both Move Method [Fowler] and Move Field [Fowler] refactorings to move the logic and the data it requires out of the Humble Dialog and into the new testable component.

以下是转换为Humble Dialog 的相同视图:

Here's the same view converted to a Humble Dialog:

  ' 接口方法,TestDirector 将调用此方法

  ' 显示结果。Public

Sub ShowResultEx(TestSetKey As TdTestSetKey, _

                                           TSTestKey As TdTestKey, _

                                           ResultKey As TdResultKey)

        Dim RpbFiles As OcsRpbFiles

        Call EnsureImplExists

        Set RpbFiles = Implementation.getTestResultFileNames(ResultKey)

        ResultsFileName = RpbFiles.ActualResultFileName

        ShowFileInBrowser ResultsFileName

End Sub



Private Sub EnsureImplExists()

        If Implementation Is Nothing Then

                Set Implementation = New OcsScriptViewerImpl

        End If

End Sub

  '  Interface  method,  TestDirector  will  call  this  method

  '  to  display  the  results.

Public  Sub  ShowResultEx(TestSetKey  As  TdTestSetKey,  _

                                           TSTestKey  As  TdTestKey,  _

                                           ResultKey  As  TdResultKey)

        Dim  RpbFiles  As  OcsRpbFiles

        Call  EnsureImplExists

        Set  RpbFiles  =  Implementation.getTestResultFileNames(ResultKey)

        ResultsFileName  =  RpbFiles.ActualResultFileName

        ShowFileInBrowser  ResultsFileName

End  Sub



Private  Sub  EnsureImplExists()

        If  Implementation  Is  Nothing  Then

                Set  Implementation  =  New  OcsScriptViewerImpl

        End  If

End  Sub

 

以下是Humble ObjectOcsScriptViewerImpl调用的可测试组件:

Here's the testable component OcsScriptViewerImpl that the Humble Object calls:

' ResultViewer 实现:

公共函数 getTestResultFileNames(ResultKey 作为变体)作为 OcsRpbFiles

        On Error GoTo Error



        Dim Attachments 作为集合

        Dim thisTest 作为运行

        Dim RpbFiles 作为新的 OcsRpbFiles



        调用 EnsureConnectedToTd



        设置 Attachments = testManager.GetAllAttachmentsOfRunTest(ResultKey)

        调用 RpbFiles.LoadFromCollection(Attachments,“RunTest”)

        设置 getTestResultFileNames = RpbFiles

        退出函数

错误:

        ' 做某事...

结束函数

'    ResultViewer  Implementation:

Public  Function  getTestResultFileNames(ResultKey  As  Variant)  As  OcsRpbFiles

        On  Error  GoTo  Error



        Dim  Attachments  As  Collection

        Dim  thisTest  As  Run

        Dim  RpbFiles  As  New  OcsRpbFiles



        Call  EnsureConnectedToTd



        Set  Attachments  =  testManager.GetAllAttachmentsOfRunTest(ResultKey)

        Call  RpbFiles.LoadFromCollection(Attachments,  "RunTest")

        Set  getTestResultFileNames  =  RpbFiles

        Exit  Function

Error:

        '  do  something  ...

End  Function

 

现在我们可以OcsScriptViewerImpl轻松地实例化这个类并为其编写 VbUnit 测试。由于篇幅原因,我省略了测试,因为它们实际上并没有显示任何特别有趣的内容。

We could now instantiate this OcsScriptViewerImpl class easily and write VbUnit tests for it. I've omitted the tests for space reasons because they don't really show anything particularly interesting.

示例:Humble 事务控制器

Example: Humble Transaction Controller

事务回滚拆解第 668Humble 事务控制器的编写测试的示例

Transaction Rollback Teardown (page 668) contains an example of writing tests that bypass the Humble Transaction Controller.

进一步阅读

请参阅http://www.objectmentor.com/resources/articles/TheHumbleDialogBox.pdf了解 Michael Feathers 对Humble Dialog模式的原始描述。

See http://www.objectmentor.com/resources/articles/TheHumbleDialogBox.pdf for Michael Feathers' original write-up of the Humble Dialog pattern.

测试钩

Test Hook

我们如何设计 SUT 以便我们可以在运行时替换它的依赖项?

How do we design the SUT so that we can replace its dependencies at runtime?

我们修改了 SUT,使其在测试期间表现不同。

We modify the SUT to behave differently during the test.

图像

几乎每一段代码都依赖于其他一些类、对象、模块或过程。为了正确地对一段代码进行单元测试,我们希望将其与依赖项隔离开来。如果这些依赖项以文字类名的形式硬编码在代码中,则很难实现这种隔离。

Almost every piece of code depends on some other classes, objects, modules, or procedures. To unit-test a piece of code properly, we would like to isolate it from its dependencies. Such isolation is difficult to achieve if those dependencies are hard-coded within the code in the form of literal classnames.

测试钩子 (Test Hook)是在自动化测试过程中引入特定测试行为的“最后手段”。

Test Hook is a "method of last resort" for introducing test-specific behavior during automated testing.

工作原理

How It Works

我们通过将钩子直接放入 SUT 或 DOC 中来修改 SUT 的行为以支持测试。这种方法意味着我们使用某种testing可以在适当位置检查的标志。

We modify the behavior of the SUT to support testing by putting a hook directly into the SUT or into a DOC. This approach implies that we use some kind of testing flag that can be checked in the appropriate place.

何时使用它

When to Use It

当我们既不能使用依赖注入第 678页)也不能使用依赖查找第 686页)时,使用这种“最后手段”是合适的。在这种情况下,我们使用测试钩子,因为我们没有其他方法来解决由硬编码依赖(请参阅第 209页的难以测试的代码)引起的未测试代码(请参阅第268页的生产错误

Sometimes it is appropriate to use this "pattern of last resort" when we cannot use either Dependency Injection (page 678) or Dependency Lookup (page 686). In this situation, we use a Test Hook because we have no other way to address the Untested Code (see Production Bugs on page 268) caused by a Hard-Coded Dependency (see Hard-to-Test Code on page 209).

当我们使用不支持对象、函数指针或任何其他形式的动态绑定的过程语言进行编程时,测试钩子可能是引入测试替身第 522页)行为的唯一方法。

A Test Hook may be the only way to introduce Test Double (page 522) behavior when we are programming in a procedural language that does not support objects, function pointers, or any other form of dynamic binding.

测试钩子可以用作过渡策略,将遗留代码纳入测试范围。我们可以使用测试钩子引入可测试性,然后在重构以提高可测试性时使用这些测试作为安全网(参见第24页)。在某个时候,我们应该能够放弃需要测试钩子的初始测试,因为我们有足够的“现代”测试来保护我们。

Test Hooks can be used as a transition strategy to bring legacy code under the testing umbrella. We can introduce testability using the Test Hooks and then use those Tests as Safety Net (see page 24) while we refactor for even more testability. At some point we should be able to discard the initial round of tests that required the Test Hooks because we have enough "modern" tests to protect us.

实施说明

Implementation Notes

测试钩子模式的本质是我们在 SUT 中插入一些代码,以便对其进行测试。无论我们如何将这些代码插入 SUT,代码本身都可以

The essence of the Test Hook pattern is that we insert some code into the SUT that lets us test it. Regardless of how we insert this code into the SUT, the code itself can either

  • 将控制权转移到测试替身而不是真实对象,或者
  • Divert control to a Test Double instead of the real object, or
  • 成为真实对象中的测试替身,或者
  • Be the Test Double within the real object, or
  • 成为特定于测试的装饰器[GOF],在生产时委托给真实对象。
  • Be a test-specific Decorator [GOF] that delegates to the real object when in production.

指示测试正在进行的标志可以是编译时常量,例如,这可能导致编译器优化所有测试逻辑。在支持预处理器或编译器宏的语言中,此类构造还可用于在代码进入生产阶段之前移除测试钩子。标志的值也可以从配置数据中读取或存储在测试直接设置的全局变量中。

The flag that indicates testing is in progress can be a compile-time constant, which may, for example, cause the compiler to optimize out all the testing logic. In languages that support preprocessors or compiler macros, such constructs may also be used to remove the Test Hook before the code enters the production phase. The value of the flag can also be read in from configuration data or stored in a global variable that the test sets directly.

激励人心的例子

Motivating Example

以下测试无法“按原样”通过:

The following test cannot be made to pass "as is":

public void testDisplayCurrentTime_AtMidnight() {

      // 固定设置

      TimeDisplay sut = new TimeDisplay();

      // 练习 SUT

      String result = sut.getCurrentTimeAsHtmlFragment();

      // 验证直接输出

      String expectedTimeString =

               "<span class=\"tinyBoldText\">Midnight</span>";

      assertEquals( expectedTimeString, result);

}

public  void  testDisplayCurrentTime_AtMidnight()  {

      //  fixture  setup

      TimeDisplay  sut  =  new  TimeDisplay();

      //  exercise  SUT

      String  result  =  sut.getCurrentTimeAsHtmlFragment();

      //  verify  direct  output

      String  expectedTimeString  =

               "<span  class=\"tinyBoldText\">Midnight</span>";

      assertEquals(  expectedTimeString,  result);

}

 

此测试几乎总是会失败,因为它依赖于 DOC 将当前时间返回给 SUT。测试无法控制该组件返回的值DefaultTimeProvider。因此,只有当系统时间恰好为午夜时分,此测试才会通过。

This test almost always fails because it depends on a DOC to return the current time to the SUT. The test cannot control the values returned by that component, the DefaultTimeProvider. As a consequence, this test will pass only when the system time is exactly midnight.

公共字符串 getCurrentTimeAsHtmlFragment() {

      日历 currentTime;

      尝试 {

            currentTime = new DefaultTimeProvider().getTime();

      } catch (异常 e) {

            返回 e.getMessage();

      }

      // 等等

}

public  String  getCurrentTimeAsHtmlFragment()  {

      Calendar  currentTime;

      try  {

            currentTime  =  new  DefaultTimeProvider().getTime();

      }  catch  (Exception  e)  {

            return  e.getMessage();

      }

      //  etc.

}

 

由于 SUT 被硬编码为使用特定类来检索时间,因此我们无法用测试替身替换 DOC 。因此,此测试是不确定的,几乎无用。我们需要找到一种方法来控制 SUT 的间接输入。

Because the SUT is hard-coded to use a particular class to retrieve the time, we cannot replace the DOC with a Test Double. As a result, this test is nondeterministic and pretty much useless. We need to find a way to gain control over the indirect inputs of the SUT.

重构说明

Refactoring Notes

我们可以通过创建可签入 SUT 的标志来引入测试钩子if/then/else。然后,我们用控制结构包装生产代码,并将特定于测试的逻辑放入then子句中。

We can introduce a Test Hook by creating a flag that can be checked into the SUT. We then wrap the production code with an if/then/else control structure and put the test-specific logic into the then clause.

示例:被测系统中的测试钩子

Example: Test Hook in System Under Test

以下是为适应通过测试钩子进行测试而修改的生产代码

Here's the production code modified to accommodate testing via a Test Hook:

public String getCurrentTimeAsHtmlFragment() {

      日历 theTime;

      尝试 {

            如果(TESTING){

                theTime = new GregorianCalendar();

                theTime.set(Calendar.HOUR_OF_DAY, 0);

                theTime.set(Calendar.MINUTE, 0);}

            else {

                theTime = new DefaultTimeProvider().getTime();

            }

      } catch (Exception e) {

            return e.getMessage();

      }

      // 等等。

public  String  getCurrentTimeAsHtmlFragment()  {

      Calendar  theTime;

      try  {

            if  (TESTING)  {

                theTime  =  new  GregorianCalendar();

                theTime.set(Calendar.HOUR_OF_DAY,  0);

                theTime.set(Calendar.MINUTE,  0);}

            else  {

                theTime  =  new  DefaultTimeProvider().getTime();

            }

      }  catch  (Exception  e)  {

            return  e.getMessage();

      }

      //  etc.

 

这里我们将testing标志实现为全局常量,我们可以根据需要对其进行编辑。这种灵活性意味着需要对要测试的系统版本进行单独的构建步骤。这种策略比使用动态配置参数或成员变量更安全,因为许多编译器会直接从目标代码中优化此钩子。

Here we have implemented the testing flag as global constant, which we can edit as necessary. This flexibility implies a separate build step is necessary for versions of the system to be tested. Such a strategy is somewhat safer than using a dynamic configuration parameter or member variable because many compilers will optimize this hook right out of the object code.

示例:依赖组件中的测试钩子

Example: Test Hook in Depended-on Component

我们还可以通过将钩子放入 DOC 而不是 SUT 来引入测试钩子:

We can also introduce a Test Hook by putting the hook into a DOC rather than into the SUT:

public Calendar getTime() throws TimeProviderEx {

      Calendar theTime = new GregorianCalendar();

      if (TESTING) {

          theTime.set(Calendar.HOUR_OF_DAY, 0);

          theTime.set(Calendar.MINUTE, 0);}

      else {

            // 只返回日历

      }

      return theTime;

};

public  Calendar  getTime()  throws  TimeProviderEx  {

      Calendar  theTime  =  new  GregorianCalendar();

      if  (TESTING)  {

          theTime.set(Calendar.HOUR_OF_DAY,  0);

          theTime.set(Calendar.MINUTE,  0);}

      else  {

            //  just  return  the  calendar

      }

      return  theTime;

};

 

这种方法稍微好一些,因为我们在测试时不会修改 SUT。

This approach is somewhat better because we are not modifying the SUT as we test it.

第 27 章

价值模式

Chapter 27

Value Patterns

 

本章中的模式

Patterns in This Chapter

文字值 674

Literal Value 714

派生值 718

Derived Value 718

生成值 723

Generated Value 723

虚拟对象 728

Dummy Object 728

文字值

Literal Value

也称为

Also known as

硬编码值,常量值

Hard-Coded Value, Constant Value

我们如何指定测试中要使用的值?

How do we specify the values to be used in tests?

我们使用文字常量来表示对象属性和断言。

We use literal constants for object attributes and assertions.

BigDecimal 预期总数 = 新 BigDecimal("99.95");

BigDecimal  expectedTotal  =  new  BigDecimal("99.95");

我们用于测试装置中对象属性的值和我们测试的预期结果通常以需求中定义的方式相互关联。正确获取这些值(尤其是先决条件和后置条件之间的关系)至关重要,因为它会将正确的行为引入 SUT。

The values we use for the attributes of objects in our test fixture and the expected outcome of our test are often related to one another in a way that is defined in the requirements. Getting these values—and, in particular, the relationship between the pre-conditions and the post-conditions—right is crucial because it drives the correct behavior into the SUT.

文字值是指定测试中对象属性值的一种常用方法。

Literal Values are a popular way to specify the values of attributes of objects in a test.

工作原理

How It Works

我们对对象的每个属性使用适当类型的文字常量,或将其用作对 SUT 或断言方法(第 362页) 的方法调用的参数。预期值通过手工、计算器或电子表格计算,并在测试中硬编码为文字值

We use a literal constant of the appropriate type for each attribute of an object or for use as an argument of a method call to the SUT or an Assertion Method (page 362). The expected values are calculated by hand, calculator, or spreadsheet and hard-coded within the test as Literal Values.

何时使用它

When to Use It

使用内联文字值可以非常清楚地显示正在使用哪个值;毫无疑问该值的身份,因为它就在我们面前。不幸的是,使用文字值会让我们难以看清测试中各个位置使用的值之间的关系,这反过来可能会导致模糊测试第 186页)。如果测试要求指定要使用哪些值,并且我们想清楚地表明我们确实在使用这些值,那么使用文字值肯定是有意义的。[我们有时可能会考虑使用数据驱动测试第 288页),以避免将数据复制到测试方法中所带来的工作量和抄写错误。]

Using a Literal Value in-line makes it very clear which value is being used; there is no doubt about the value's identity because it is right in front of our face. Unfortunately, using Literal Values can make it difficult to see the relationships between the values used in various places in the test, which may in turn lead to Obscure Tests (page 186). It certainly makes sense to use Literal Values if the testing requirements specify which values are to be used and we want to make it clear that we are, in fact, using those values. [We might sometimes consider using a Data-Driven Test (page 288) instead to avoid the effort and transcription errors associated with copying the data into test methods.]

使用文字值的一个缺点是,我们可能会对两个不相关的属性使用相同的值;如果 SUT 恰好使用了错误的值,测试可能会通过,即使它们不应该通过。如果文字值是文件名或用于访问数据库的键,则值的含义会​​丢失 — 文件或记录的内容实际上驱动了 SUT 的行为。在这种情况下,使用文字值作为键无助于帮助读者理解测试,我们可能会遭受模糊测试的困扰。

One downside of using a Literal Value is that we might use the same value for two unrelated attributes; if the SUT happens to use the wrong one, tests may pass even though they should not. If the Literal Value is a filename or a key used to access a database, the meaning of the value is lost—the content of the file or record actually drives the behavior of the SUT. Using a Literal Value as the key does nothing to help the reader understand the test in such a case, and we are likely to suffer from Obscure Tests.

如果预期结果中的值可以从夹具设置逻辑中的值中派生出来,如果我们使用派生值第 718页),我们将更有可能使用测试作为文档(参见第 23页)。相反,如果这些值对于被测试逻辑的规范并不重要,我们应该考虑使用生成的值第 723页)。

If the values in the expected outcome can be derived from the values in the fixture setup logic, we will be more likely to use the Tests as Documentation (see page 23) if we use Derived Values (page 718). Conversely, if the values are not important to the specification of the logic being tested, we should consider using Generated Values (page 723).

实施说明

Implementation Notes

使用文字值的最常见方式是在代码中使用文字常量。当需要在测试中的多个位置使用相同的值时(通常在夹具设置和结果验证期间),这种方法可能会模糊测试先决条件和后置条件之间的关系。引入一个命名醒目的符号常量可以使这种关系更加清晰。同样,如果我们不能使用自描述值,我们仍然可以通过定义一个适当命名的符号常量并在任何需要使用文字值的地方使用它来使代码更易于使用

The most common way to use a Literal Value is with literal constants within the code. When the same value needs to be used in several places in the test (typically during fixture setup and result verification), this approach can obscure the relationship between the test pre-conditions and post-conditions. Introducing an evocatively named symbolic constant can make this relationship much clearer. Likewise, if we cannot use a self-describing value, we can still make the code easier to use by defining a suitably named symbolic constant and using it wherever we would have used the Literal Value.

变量:符号常数

当我们需要在单个测试方法(第 348页) 或多个不同测试中的多个位置使用相同的文字值时,最好使用符号常量而不是文字值符号常量在功能上等同于文字值,但降低了高测试维护成本(第265页)的可能性。

When we need to use the same Literal Value in several places in a single Test Method (page 348) or within several distinct tests, it is a good practice to use a Symbolic Constant instead of a Literal Value. A Symbolic Constant is functionally equivalent to a Literal Value but reduces the likelihood of High Test Maintenance Cost (page 265).

变化:自我描述值

当一个对象的多个属性需要同一种类型的值时,使用不同的值可以带来优势,因为它可以帮助我们证明 SUT 正在使用正确的属性。当属性或参数是不受约束的字符串时,选择一个描述该值在测试中的作用的值(自描述值)会很有用。例如,使用“不是现有客户”作为客户名称可能比使用“Joe Blow”对读者更有帮助,尤其是在我们调试时或当属性包含在测试失败输出中时。

When several attributes of an object need the same kind of value, using different values provides advantages by helping us to prove that the SUT is working with the correct attribute. When an attribute or argument is an unconstrained string, it can be useful to choose a value that describes the role of the value in the test (a Self-Describing Value). For example, using "Not an existing customer" for the name of a customer might be more helpful to the reader than using "Joe Blow," especially when we are debugging or when the attributes are included in the test failure output.

示例:文字值

Example: Literal Value

因为Literal Value通常是编写测试的起点,所以我将省去一个激励性的例子,直接切入正题。下面是Literal Value模式的实际示例。请注意在装置设置逻辑和断言中都使用了Literal Values 。

Because Literal Value is usually the starting point when writing tests, I'll dispense with a motivating example and cut straight to the chase. Here's an example of the Literal Value pattern in action. Note the use of Literal Values in both the fixture setup logic and the assertion.

public void testAddItemQuantity_1() throws Exception {

      Product product = new Product("Widget", 19.95);

      Invoice invoice = new Invoice();

      // 练习

      invoice.addItemQuantity(product, 1);

      // 验证

      列表 lineItems = invoice.getLineItems();

      LineItem actualItem = (LineItem)lineItems.get(0);

      assertEquals(new BigDecimal("19.95"),

                          actualItem.getExtendedPrice());

}

public  void  testAddItemQuantity_1()  throws  Exception  {

      Product  product  =  new  Product("Widget",  19.95);

      Invoice  invoice  =  new  Invoice();

      //  Exercise

      invoice.addItemQuantity(product,  1);

      //  Verify

      List  lineItems  =  invoice.getLineItems();

      LineItem  actualItem  =  (LineItem)lineItems.get(0);

      assertEquals(new  BigDecimal("19.95"),

                          actualItem.getExtendedPrice());

}

 

构造函数Product需要名称和成本。extendedCost的断言lineItem需要该行项目的产品总成本值。在此示例中,我们将这些值作为硬编码文字常量包含在内。在下一个示例中,我们将改用符号常量。

The Product constructor requires both a name and a cost. The assertion on the extendedCost of the lineItem requires a value for the total cost of the product for that line item. In this example, we included these values as hard-coded literal constants. In the next example, we'll use symbolic constants instead.

重构说明

Refactoring Notes

我们可以通过执行用符号常量替换魔法数字 [Fowler] 重构来减少以硬编码文字值形式出现的测试代码重复第 213页)。19.95

We can reduce the Test Code Duplication (page 213) in the form of the hard-coded Literal Value of 19.95 by doing a Replace Magic Number with Symbolic Constant [Fowler] refactoring.

例如:符号常数

Example: Symbolic Constant

原始测试的重构版本将小部件价格的重复文字值(19.95)替换为适当命名的符号常量,该符号常量在夹具设置和结果验证期间使用:

This refactored version of the original test replaces the duplicated Literal Value of the widget's price (19.95) with a suitably named Symbolic Constant that is used during fixture setup as well as result verification:

public void testAddItemQuantity_1s() throws Exception {

      BigDecimal widgetPrice = new BigDecimal("19.95");

      Product product = new Product("Widget", widgetPrice);

      Invoice invoice = new Invoice();

      // 练习

      invoice.addItemQuantity(product, 1);

      // 验证

      列表 lineItems = invoice.getLineItems();

      LineItem actualItem = (LineItem)lineItems.get(0);

      assertEquals(widgetPrice, actualItem.getExtendedPrice());

}

public  void  testAddItemQuantity_1s()  throws  Exception  {

      BigDecimal  widgetPrice  =  new  BigDecimal("19.95");

      Product  product  =  new  Product("Widget",  widgetPrice);

      Invoice  invoice  =  new  Invoice();

      //  Exercise

      invoice.addItemQuantity(product,  1);

      //  Verify

      List  lineItems  =  invoice.getLineItems();

      LineItem  actualItem  =  (LineItem)lineItems.get(0);

      assertEquals(widgetPrice,  actualItem.getExtendedPrice());

}

 

示例:自我描述价值

Example: Self-Describing Value

此测试的重构版本为传递给构造函数的强制名称参数提供了一个自描述值Product。我们正在测试的方法不会使用此值;它只是被存储起来,以供我们在此未测试的另一种方法稍后访问。

This refactored version of the test provides a Self-Describing Value for the mandatory name argument passed to the Product constructor. This value is not used by the method we are testing; it is merely stored for later access by another method we are not testing here.

public void testAddItemQuantity_1b() throws Exception {

      BigDecimal widgetPrice = new BigDecimal("19.95");

      Product product = new Product("不相关的产品名称",

                                                           widgetPrice);

      Invoice invoice = new Invoice();

      // 练习

      invoice.addItemQuantity(product, 1);

      // 验证

      列表 lineItems = invoice.getLineItems();

      LineItem actualItem = (LineItem)lineItems.get(0);

      assertEquals(widgetPrice, actualItem.getExtendedPrice());

}

public  void  testAddItemQuantity_1b()  throws  Exception  {

      BigDecimal  widgetPrice  =  new  BigDecimal("19.95");

      Product  product  =  new  Product("Irrelevant  product  name",

                                                           widgetPrice);

      Invoice  invoice  =  new  Invoice();

      //  Exercise

      invoice.addItemQuantity(product,  1);

      //  Verify

      List  lineItems  =  invoice.getLineItems();

      LineItem  actualItem  =  (LineItem)lineItems.get(0);

      assertEquals(widgetPrice,  actualItem.getExtendedPrice());

}

 

示例:不同值

Example: Distinct Value

此测试需要验证商品名称是否取自产品名称。我们将对名称和 SKU 使用不同的值,以便区分它们。

This test needs to verify that the item's name is taken from the product's name. We'll use a Distinct Value for the name and the SKU so we can tell them apart.

public void testAddItemQuantity_1c() throws Exception {

      BigDecimal widgetPrice = new BigDecimal("19.95");

      String name = "产品名称";

      String sku = "产品 SKU";

      Product product = new Product(name, sku, widgetPrice);

      Invoice invoice = new Invoice();

      // 练习

      invoice.addItemQuantity(product, 1);

      // 验证

      列表 lineItems = invoice.getLineItems();

      LineItem actualItem = (LineItem)lineItems.get(0);

      assertEquals(name, actualItem.getName());

}

public  void  testAddItemQuantity_1c()  throws  Exception  {

      BigDecimal  widgetPrice  =  new  BigDecimal("19.95");

      String  name  =  "Product  name";

      String  sku  =  "Product  SKU";

      Product  product  =  new  Product(name,  sku,  widgetPrice);

      Invoice  invoice  =  new  Invoice();

      //  Exercise

      invoice.addItemQuantity(product,  1);

      //  Verify

      List  lineItems  =  invoice.getLineItems();

      LineItem  actualItem  =  (LineItem)lineItems.get(0);

      assertEquals(name,  actualItem.getName());

}

 

这也恰好是一个自我描述价值的例子。

This also happens to be an example of a self-describing value.

派生值

Derived Value

也称为

Also known as

计算值

Calculated Value

我们如何指定测试中要使用的值?

How do we specify the values to be used in tests?

我们使用表达式来计算可以从其他值得出的值。

We use expressions to calculate values that can be derived from other values.

BigDecimal 预期总额 = itemPrice.multiply(数量);

BigDecimal  expectedTotal  =  itemPrice.multiply(QUANTITY);

我们用于测试装置中对象属性的值以及测试结果验证部分的值通常以需求中定义的方式相互关联。正确获取这些值(尤其是先决条件和后置条件之间的关系)至关重要,因为它会将正确的行为引入 SUT,并帮助测试充当我们软件的文档。

The values we use for the attributes of objects in our test fixtures and the result verification parts of our tests are often related to one another in a way that is defined in the requirements. Getting these values—and, in particular, the relationship between the pre-conditions and the post-conditions—right is crucial because it drives the correct behavior into the SUT and helps the tests act as documentation of our software.

通常,其中一些值可以从同一测试中的其他值中得出。在这些情况下,如果我们通过使用适当的表达式计算值来显示推导过程,那么使用我们的测试作为文档(参见第 23页)的好处就会得到改善。

Often, some of these values can be derived from other values in the same test. In these cases the benefits from using our Tests as Documentation (see page 23) are improved if we show the derivation by calculating the values using the appropriate expression.

工作原理

How It Works

计算机非常擅长数学和字符串连接。我们可以将预期结果的数学运算直接编码为断言方法(第 362页) 调用的参数,从而避免在脑中(或使用计算器)进行数学运算。我们还可以使用派生值作为夹具对象创建的参数,并在执行 SUT 时将其用作方法参数。

Computers are really good at math and string concatenation. We can avoid doing the math in our head (or with a calculator) by coding the math for expected results as arguments of the Assertion Method (page 362) calls directly into the tests. We can also use Derived Values as arguments for fixture object creation and as method arguments when exercising the SUT.

派生值本质上鼓励我们使用变量或符号常量来保存值。这些变量/常量可以在编译时(常量)、类或测试用例对象(第 382页) 初始化期间、夹具设置期间或在测试方法(第 348页)主体内初始化

Derived Values, by their very nature, encourage us to use variables or symbolic constants to hold the values. These variables/constants can be initialized at compile time (constants), during class or Testcase Object (page 382) initialization, during fixture setup, or within the body of the Test Method (page 348).

何时使用它

When to Use It

每当我们有可以以某种确定性方式从测试中的其他值派生的值时,我们就应该使用派生值。使用派生值的主要缺点是相同的数学错误(例如舍入误差)可能同时出现在 SUT 和测试中。为了安全起见,我们可能希望使用文字值第 714页)编写一些病态测试用例,以防出现此类问题。如果我们使用的值必须是唯一的或不影响 SUT 中的逻辑,我们最好使用生成的值第 723页)。

We should use a Derived Value whenever we have values that can be derived in some deterministic way from other values in our tests. The main drawback of using Derived Values is that the same math error (e.g., rounding errors) could appear in both the SUT and the tests. To be safe, we might want to code a few of the pathological test cases using Literal Values (page 714) just in case such a problem might be present. If the values we are using must be unique or don't affect the logic in the SUT, we may be better off using Generated Values (page 723) instead.

我们可以将派生值用作夹具设置的一部分(派生输入一个错误属性),或者在确定要与 SUT 生成的值进行比较的预期值时使用派生值(派生期望)。本节后面将更详细地介绍这些用途。

We can use a Derived Value either as part of fixture setup (Derived Input or One Bad Attribute) or when determining the expected values to be compared with those generated by the SUT (Derived Expectation). These uses are described in a bit more detail later in this section.

变体:派生输入

有时,我们的测试装置包含类似的值,SUT 可能会比较这些值或使用这些值来根据它们之间的差异制定逻辑。例如,在测试的装置设置部分,可以通过将差异添加到基值来计算派生输入。此操作使两个值之间的关系变得明确。我们甚至可以将要添加的值放在具有意图揭示名称[SBPP]的符号常量中,例如MAXIMUM_ALLOWABLE_TIME_DIFFERENCE

Sometimes our test fixture contains similar values that the SUT might compare or use to base its logic on the difference between them. For example, a Derived Input might be calculated in the fixture setup portion of the test by adding the difference to a base value. This operation makes the relationship between the two values explicit. We can even put the value to be added in a symbolic constant with an Intent-Revealing Name [SBPP] such as MAXIMUM_ALLOWABLE_TIME_DIFFERENCE.

变体:一个不良属性

当我们需要测试一个以复杂对象为参数的方法时,通常会使用派生输入。例如,彻底的“输入验证”测试要求我们在将对象的每个属性设置为一个或多个可能的无效值的情况下执行该方法,以确保它正确处理所有这些情况。由于第一个被拒绝的值可能会导致方法终止,因此我们必须在对 SUT 的单独调用中验证每个坏属性;反过来,每个调用都应该在单独的测试方法中完成(每个调用都应该是单条件测试;参见第45页)。我们可以通过首先创建一个有效对象,然后用无效值替换其属性之一来轻松实例化无效对象。最好使用创建方法第 415页)创建有效对象,以避免测试代码重复第 213页)。

A Derived Input is often employed when we need to test a method that takes a complex object as an argument. For example, thorough "input validation" testing requires that we exercise the method with each of the attributes of the object set to one or more possible invalid values to ensure that it handles all of these cases correctly. Because the first rejected value could cause termination of the method, we must verify each bad attribute in a separate call to the SUT; each of these calls, in turn, should be done in a separate test method (each should be a Single-Condition Test; see page 45). We can instantiate the invalid object easily by first creating a valid object and then replacing one of its attributes with an invalid value. It is best to create the valid object using a Creation Method (page 415) so as to avoid Test Code Duplication (page 213).

变体:导出期望

当 SUT 产生的某个值应该与我们作为参数或夹具中的值传递给 SUT 的一个或多个值相关时,我们通常可以在测试执行时从输入值中得出预期值,而不是使用预先计算的文字值。然后,我们将结果用作相等断言中的预期值(参见断言方法)。

When some value produced by the SUT should be related to one or more of the values we passed in to the SUT as arguments or as values in the fixture, we can often derive the expected value from the input values as the test executes rather than using precalculated Literal Values. We then use the result as the expected value in an Equality Assertion (see Assertion Method).

激励人心的例子

Motivating Example

以下测试不使用派生值。请注意在夹具设置逻辑和断言中都使用了文字值。

The following test doesn't use Derived Values. Note the use of Literal Values in both the fixture setup logic and the assertion.

public void testAddItemQuantity_2a() throws Exception {

      BigDecimal widgetPrice = new BigDecimal("19.99");



      Product product = new Product("Widget", widgetPrice);

      Invoice invoice = new Invoice();

      // 练习

      invoice.addItemQuantity(product, 5);

      // 验证

      列表 lineItems = invoice.getLineItems();

      LineItem actualItem = (LineItem)lineItems.get(0);

      assertEquals(new BigDecimal("99.95"),

                           actualItem.getExtendedPrice());

}

public  void  testAddItemQuantity_2a()  throws  Exception  {

      BigDecimal  widgetPrice  =  new  BigDecimal("19.99");



      Product  product  =  new  Product("Widget",  widgetPrice);

      Invoice  invoice  =  new  Invoice();

      //  Exercise

      invoice.addItemQuantity(product,  5);

      //  Verify

      List  lineItems  =  invoice.getLineItems();

      LineItem  actualItem  =  (LineItem)lineItems.get(0);

      assertEquals(new  BigDecimal("99.95"),

                           actualItem.getExtendedPrice());

}

 

测试读者可能需要在脑海中进行一些数学运算才能充分理解夹具设置中的值与测试结果验证部分中的值之间的关系。

Test readers may have to do some math in their heads to fully appreciate the relationship between the values in the fixture setup and the value in the result verification part of the test.

重构说明

Refactoring Notes

为了使这个测试更具可读性,我们可以用计算这些值的公式替换任何实际上从其他值派生出来的文字值。

To make this test more readable, we can replace any Literal Values that are actually derived from other values with formulas that calculate these values.

示例:导出期望

Example: Derived Expectation

原始示例仅包含五个产品实例中的一个项目。因此,我们通过将单价乘以数量来计算扩展价格属性的预期值,这使得值之间的关系变得明确。

The original example contained only one line item for five instances of the product. We therefore calculated the expected value of the extended price attribute by multiplying the unit price by the quantity, which makes the relationship between the values explicit.

public void testAddItemQuantity_2b() throws Exception {

      BigDecimal widgetPrice = new BigDecimal("19.99");

      BigDecimal numberOfUnits = new BigDecimal("5");

      Product product = new Product("Widget", widgetPrice);

      Invoice invoice = new Invoice();

      // 练习

      invoice.addItemQuantity(product, numberOfUnits);

      // 验证

      列表 lineItems = invoice.getLineItems();

      LineItem actualItem = (LineItem)lineItems.get(0);

      BigDecimal totalPrice = widgetPrice.multiply(numberOfUnits);

      assertEquals(totalPrice, actualItem.getExtendedPrice());

}

public  void  testAddItemQuantity_2b()  throws  Exception  {

      BigDecimal  widgetPrice  =  new  BigDecimal("19.99");

      BigDecimal  numberOfUnits  =  new  BigDecimal("5");

      Product  product  =  new  Product("Widget",  widgetPrice);

      Invoice  invoice  =  new  Invoice();

      //  Exercise

      invoice.addItemQuantity(product,  numberOfUnits);

      //  Verify

      List  lineItems  =  invoice.getLineItems();

      LineItem  actualItem  =  (LineItem)lineItems.get(0);

      BigDecimal  totalPrice  =  widgetPrice.multiply(numberOfUnits);

      assertEquals(totalPrice,  actualItem.getExtendedPrice());

}

 

请注意,我们还引入了单价和数量的符号常量,以使表达更加明显,并减少以后更改值的工作量。

Note that we have also introduced symbolic constants for the unit price and quantity to make the expression even more obvious and to reduce the effort of changing the values later.

示例:一个错误属性

Example: One Bad Attribute

假设我们有以下客户工厂方法[GOF],它以CustomerDto对象作为参数。我们想要编写测试来验证当我们为 中的每个属性传递无效值时会发生什么。我们可以在每个测试方法CustomerDto中创建CustomerDto内联,并将相应的属性初始化为某个无效值。

Suppose we have the following Customer Factory Method [GOF], which takes a CustomerDto object as an argument. We want to write tests to verify what occurs when we pass in invalid values for each of the attributes in the CustomerDto. We could create the CustomerDto in-line in each Test Method with the appropriate attribute initialized to some invalid value.

public void testCreateCustomerFromDto_BadCredit() {

        // 固定设置

        CustomerDto customerDto = new CustomerDto();

        customerDto.firstName = "xxx";

        customerDto.lastName = "yyy";

        // 等等

        customerDto.address = createValidAddress();

        customerDto.creditRating = CreditRating.JUNK;

        // 锻炼 SUT

        try {

              sut.createCustomerFromDto(customerDto);

              fail("Expected an exception");

        } catch (InvalidInputException e) {

              assertEquals( "Field", "Credit", e.field );

        }

}



public void testCreateCustomerFromDto_NullAddress() {

        // 固定设置

        CustomerDto customerDto = new CustomerDto();

        customerDto.firstName = "xxx";

        customerDto.lastName = "yyy";

        // 等等

        customerDto.address = null;

        customerDto.creditRating = CreditRating.AAA;

        // 练习 SUT

        尝试 {

              sut.createCustomerFromDto(customerDto);

              fail("预期异常");

        } catch (InvalidInputException e) {

              assertEquals( "Field", "Address", e.field );

        }

}

public  void  testCreateCustomerFromDto_BadCredit()  {

        //  fixture  setup

        CustomerDto  customerDto  =  new  CustomerDto();

        customerDto.firstName  =  "xxx";

        customerDto.lastName  =  "yyy";

        //  etc.

        customerDto.address  =  createValidAddress();

        customerDto.creditRating  =  CreditRating.JUNK;

        //  exercise  the  SUT

        try  {

              sut.createCustomerFromDto(customerDto);

              fail("Expected  an  exception");

        }  catch  (InvalidInputException  e)  {

              assertEquals(  "Field",  "Credit",  e.field  );

        }

}



public  void  testCreateCustomerFromDto_NullAddress()  {

        //  fixture  setup

        CustomerDto  customerDto  =  new  CustomerDto();

        customerDto.firstName  =  "xxx";

        customerDto.lastName  =  "yyy";

        //  etc.

        customerDto.address  =  null;

        customerDto.creditRating  =  CreditRating.AAA;

        //  exercise  the  SUT

        try  {

              sut.createCustomerFromDto(customerDto);

              fail("Expected  an  exception");

        }  catch  (InvalidInputException  e)  {

              assertEquals(  "Field",  "Address",  e.field  );

        }

}

 

此代码的明显问题是,我们最终会得到大量重复测试代码,因为每个属性至少需要一个测试。如果我们进行增量开发,问题会变得更加严重:我们将需要为每个新添加的属性进行更多测试,并且我们必须重新访问所有现有测试以将新属性添加到工厂方法签名中。

The obvious problem with this code is that we end up with a lot of Test Code Duplication because we need at least one test per attribute. The problem becomes even worse if we are doing incremental development: We will require more tests for each newly added attribute, and we will have to revisit all existing tests to add the new attribute to the Factory Method signature.

解决方案是定义一个创建方法,该方法生成一个有效的实例CustomerDto(通过对其中一个测试执行提取方法 [Fowler] 重构),并在每个测试中使用它来创建一个有效的DTO。然后我们只需在每个测试中将其中一个属性替换为无效值即可。现在每个测试都有一个具有一个错误属性的对象,每个属性的无效方式略有不同。

The solution is to define a Creation Method that produces a valid instance of the CustomerDto (by doing an Extract Method [Fowler] refactoring on one of the tests) and uses it in each test to create a valid DTO. Then we simply replace one of the attributes with an invalid value in each of the tests. Each test now has an object with One Bad Attribute, with each one invalid in a slightly different way.

public void testCreateCustomerFromDto_BadCredit_OBA() {

      CustomerDto customerDto = createValidCustomerDto();

      customerDto.creditRating = CreditRating.JUNK;

      try {

            sut.createCustomerFromDto(customerDto);

            fail("预期出现异常");

        } catch (InvalidInputException e) {

            assertEquals( "Field", "Credit", e.field );

        }

}



public void testCreateCustomerFromDto_NullAddress_OBA() {

      CustomerDto customerDto = createValidCustomerDto();

      customerDto.address = null;

      try {

            sut.createCustomerFromDto(customerDto);

            fail("预期出现异常");

      } catch (InvalidInputException e) {

            assertEquals( "Field", "Address", e.field );

      }

}

public  void  testCreateCustomerFromDto_BadCredit_OBA()  {

      CustomerDto  customerDto  =  createValidCustomerDto();

      customerDto.creditRating  =  CreditRating.JUNK;

      try  {

            sut.createCustomerFromDto(customerDto);

            fail("Expected  an  exception");

        }  catch  (InvalidInputException  e)  {

            assertEquals(  "Field",  "Credit",  e.field  );

        }

}



public  void  testCreateCustomerFromDto_NullAddress_OBA()  {

      CustomerDto  customerDto  =  createValidCustomerDto();

      customerDto.address  =  null;

      try  {

            sut.createCustomerFromDto(customerDto);

            fail("Expected  an  exception");

      }  catch    (InvalidInputException  e)  {

            assertEquals(  "Field",  "Address",  e.field  );

      }

}

 

创造价值

Generated Value

我们如何指定测试中要使用的值?

How do we specify the values to be used in tests?

每次运行测试时,我们都会生成一个合适的值。

BigDecimal  uniqueCustomerNumber  =  getUniqueNumber();

We generate a suitable value each time the test is run.

BigDecimal  uniqueCustomerNumber  =  getUniqueNumber();

在初始化测试装置中的对象时,必须处理的一个问题是,大多数对象都有各种属性(字段),需要将其作为参数提供给构造函数。有时,要使用的确切值会影响测试的结果。然而,通常情况下,重要的是每个对象使用不同的值。当这些属性的精确值对测试并不重要时,重要的是不要让它们在测试中可见!

When initializing the objects in the test fixture, one issue that must be dealt with is the fact that most objects have various attributes (fields) that need to be supplied as arguments to the constructor. Sometimes the exact values to be used affect the outcome of the test. More often than not, however, it is important only that each object use a different value. When the precise values of these attributes are not important to the test, it is important not to have them visible within the test!

生成的值与创建方法第 415页)结合使用,帮助我们从测试中删除这些可能分散注意力的信息。

Generated Values are used in conjunction with Creation Methods (page 415) to help us remove this potentially distracting information from the test.

工作原理

How It Works

我们不是在编写测​​试代码时决定在测试中使用哪些值,而是在实际执行测试时生成值。然后,我们可以选择满足特定标准的值,例如“在数据库中必须是唯一的”,而这些标准只能在测试运行过程中确定。

Instead of deciding which values to use in our tests while we are coding the tests, we generate the values when we actually execute the tests. We can then pick values to satisfy specific criteria such as "must be unique in the database" that can be determined only as the test run unfolds.

何时使用它

When to Use It

每当我们无法或不想在测试执行之前指定测试值时,我们都会使用生成值。也许某个属性的值不会影响测试的结果,而且我们不想费心定义文字值(第 714页)或者我们需要确保只能在运行时确定的属性的某些质量。在某些情况下,SUT 要求属性的值是唯一的;使用生成值可以确保满足此标准,从而通过降低测试与另一测试运行中的并行化身发生冲突的可能性来防止不可重复的测试(请参阅第 228页的异常测试)和测试运行战争(请参阅异常测试)。或者,我们可以对对象的所有属性使用这个不同的值;当我们在调试器中检查对象时,对象识别就会变得非常容易。

We use a Generated Value whenever we cannot or do not want to specify the test values until the test is executing. Perhaps the value of an attribute is not expected to affect the outcome of the test and we don't want to be bothered to define Literal Values (page 714), or perhaps we need to ensure some quality of the attribute that can be determined only at runtime. In some cases, the SUT requires the value of an attribute to be unique; using a Generated Value can ensure that this criterion is satisfied and thereby prevent Unrepeatable Tests (see Erratic Test on page 228) and Test Run Wars (see Erratic Test) by reducing the likelihood of a test conflicting with its parallel incarnation in another test run. Optionally, we can use this distinct value for all attributes of the object; object recognition then becomes very easy when we inspect the object in a debugger.

需要注意的是,不同的值可能会暴露不同的错误。例如,一位数的格式可能正确,而多位数的格式可能不正确(反之亦然)。生成的值可能会导致非确定性测试(请参阅不稳定测试);如果我们遇到不确定性(有时测试通过,然后在下一次运行中失败),我们必须检查 SUT 代码以查看值的差异是否是根本原因。

One thing to be wary of is that different values could expose different bugs. For example, a single-digit number may be formatted correctly, whereas a multidigit number might not (or vice versa). Generated Values can result in Nondeterministic Tests (see Erratic Test); if we encounter nondeterminism (sometimes the test passes and then fails during the very next run), we must check the SUT code to see whether differences in value could be the root cause.

一般来说,我们不应该使用生成的值,除非该值必须唯一,因为这样的值可能会引入不确定性。显而易见的替代方法是使用文字值。一个不太明显的替代方法是使用派生值第 718页),尤其是当我们必须确定测试的预期结果时。

In general, we shouldn't use a Generated Value unless the value must be unique because of the nondeterminism such a value may introduce. The obvious alternative is to use a Literal Value. A less obvious alternative is to use a Derived Value (page 718), especially when we must determine the expected results of a test.

实施说明

Implementation Notes

我们可以用多种方法来生成值。每种方法的适用性取决于具体情况。

We can generate values in a number of ways. The appropriateness of each technique depends on the circumstance.

变化:不同的生成值

当需要确保每个测试或对象使用不同的值时,我们可以利用不同的生成值。在这种情况下,我们可以创建一组实用函数,这些函数将返回各种类型(例如整数、字符串、浮点数)的唯一值。各种getUnique方法都可以建立在整数序列号生成器之上。对于在共享数据库范围内必须唯一的数字,我们可以使用数据库序列或序列表。对于在特定测试运行范围内必须唯一的数字,我们可以使用内存中的序列号生成器(例如,使用在使用前递增的 Java 静态变量)。每次运行测试套件时从数字 1 开始的内存序列号提供了一个有用的特性:每次测试生成的值对于每次运行都是相同的,并且可以简化调试。

When we need to ensure that each test or object uses a different value, we can take advantage of Distinct Generated Values. In such a case, we can create a set of utility functions that will return unique values of various types (e.g., integers, strings, floating-point numbers). The various getUnique methods can all be built upon an integer sequence number generator. For numbers that must be unique within the scope of a shared database, we can use database sequences or a sequence table. For numbers that must be unique within the scope of a particular test run, we can use an in-memory sequence number generator (e.g., use a Java static variable that is incremented before usage). In-memory sequence numbers that start from the number 1 each time a test suite is run offer a useful quality: The values generated in each test are the same for each run and can simplify debugging.

变化:随机生成的值

获得良好测试覆盖率而又无需花费大量时间分析行为和生成测试条件的方法之一是在每次运行测试时使用不同的值。使用随机生成的值是实现此目标的一种方法。虽然使用这样的值看起来是个好主意,但它会使测试变得不确定(非确定性测试),并使调试失败的测试变得非常困难。理想情况下,当测试失败时,我们希望能够按需重复该测试失败。为此,我们可以在运行测试时记录随机生成的值并将其显示为测试失败的一部分。然后,我们需要找到一种方法,在对失败的测试进行故障排除时强制测试再次使用该值。在大多数情况下,所需的努力超过潜在的收益。当然,当我们需要这种技术时,我们真的需要它。

One way to obtain good test coverage without spending a lot of time analyzing the behavior and generating test conditions is to use different values each time we run the tests. Using a Random Generated Value is one way to accomplish this goal. While use of such values may seem like a good idea, it makes the tests nondeterministic (Nondeterministic Tests) and can make debugging failed tests very difficult. Ideally, when a test fails, we want to be able to repeat that test failure on demand. To do so, we can log the Random Generated Value as the test is run and show it as part of the test failure. We then need to find a way to force the test to use that value again while we are troubleshooting the failed test. In most cases, the effort required outweighs the potential benefit. Of course, when we need this technique, we really need it.

变化:相关生成值

可选的增强功能是将生成的值派生值结合起来,方法是将同一个生成的整数用作单个对象所有属性的根。只需调用getUniqueInt一次,然后使用该值构建唯一字符串、浮点数和其他值即可实现此结果。使用相关生成的值generateNewUniqueRoot,对象的所有字段都包含“相关”数据,这使得调试时更容易识别对象。另一个选项是在调用之前显式调用,将根的生成与值的生成分开getUniqueInt,  getUniqueString,依此类推。

An optional enhancement is to combine a Generated Value with a Derived Value by using the same generated integer as the root for all attributes of a single object. This result can be accomplished by calling getUniqueInt once and then using that value to build unique strings, floating-point numbers, and other values. With a Related Generated Value, all fields of the object contain "related" data, which makes the object easier to recognize when debugging. Another option is to separate the generation of the root from the generation of the values by calling generateNewUniqueRoot explicitly before calling getUniqueInt,  getUniqueString, and so on.

字符串的另一个好处是将一个描述角色的参数传递给函数,该参数与唯一整数键相结合,使代码更能揭示意图。虽然我们也可以将这样的参数传递给其他函数,但我们当然不能将它们构建为整数值。

Another nice touch for strings is to pass a role-describing argument to the function that is combined with the unique integer key to make the code more intent-revealing. Although we could also pass such arguments to the other functions, of course we wouldn't be able to build them into an integer value.

激励人心的例子

Motivating Example

以下测试使用文字值作为构造函数的参数:

The following test uses Literal Values for the arguments to a constructor:

public void testProductPrice_HCV() {

      // 设置

      产品 product =

            new Product( 88, // ID

                                    "Widget", // 名称

                                      new BigDecimal("19.99")); // 价格

      // 练习

      // ...

}

public  void  testProductPrice_HCV()  {

      //      Setup

      Product  product  =

            new  Product(  88,                                          //  ID

                                    "Widget",                                //  Name

                                      new  BigDecimal("19.99"));  //  Price

      //  Exercise

      //      ...

}

 

重构说明

Refactoring Notes

我们可以通过将文字值替换为对适当方法的调用,将测试转换为使用不同生成的值。这些方法每次被调用时都会增加一个计数器,并使用该计数器值作为构造适当类型值的根。getUnique

We can convert the test to use Distinct Generated Values by replacing the Literal Values with calls to the appropriate getUnique method. These methods simply increment a counter each time they are called and use that counter value as the root for construction of an appropriately typed value.

示例:独特的生成值

Example: Distinct Generated Value

以下是使用Distinct Generated Value 的相同测试。对于该getUniqueString方法,我们将传递一个描述角色的字符串(“Widget Name”)。

Here is the same test using a Distinct Generated Value. For the getUniqueString method, we'll pass a string describing the role ("Widget Name").

public void testProductPrice_DVG() {

    // 设置

    产品 product =

    new Product( getUniqueInt(), // ID

                            getUniqueString("Widget"), // 名称

                            getUniqueBigDecimal()); // 价格

    // 练习

    // ...

}



static int counter = 0;



int getUniqueInt() {

      counter++;

      return counter;

}



BigDecimal getUniqueBigDecimal() {

      return new BigDecimal(getUniqueInt());

}



String getUniqueString(String baseName) {

      return baseName.concat(String.valueOf( getUniqueInt()));

}

public  void  testProductPrice_DVG()  {

    //  Setup

    Product  product  =

    new  Product(  getUniqueInt(),                         //  ID

                            getUniqueString("Widget"),  //  Name

                            getUniqueBigDecimal());      //  Price

    //  Exercise

    //      ...

}



static  int  counter  =  0;



int  getUniqueInt()  {

      counter++;

      return  counter;

}



BigDecimal  getUniqueBigDecimal()  {

      return  new  BigDecimal(getUniqueInt());

}



String  getUniqueString(String  baseName)  {

      return  baseName.concat(String.valueOf(  getUniqueInt()));

}

 

此测试对构造函数调用的每个参数使用不同的生成值。以这种方式生成的数字是连续的,但测试读取者在调试时仍需要查看特定属性以获得一致的视图。如果我们测试的逻辑与价格计算相关,我们可能不应该生成价格值,因为这会迫使我们的验证逻辑适应不同的总成本。

This test uses a different generated value for each argument of the constructor call. The numbers generated in this way are consecutive but the test reader still needs to look at a specific attribute when debugging to get a consistent view. We probably should not generate the price value if the logic we were testing was related to price calculation because that would force our verification logic to accommodate different total costs.

示例:相关生成值

Example: Related Generated Value

通过将根值的生成与各个值的构造分开,我们可以确保测试使用的所有值都明显相关。在下面的示例中,我们将根的生成移到了setUp方法中,因此每个测试方法只会获得一次新值。检索各种值的方法(例如)在派生生成的值getUniqueString时只需使用先前生成的根。

We can ensure that all values used by the test are obviously related by separating the generation of the root value from the construction of the individual values. In the following example, we've moved the generation of the root to the setUp method so each test method gets a new value only once. The methods that retrieve the various values (e.g., getUniqueString) simply use the previously generated root when deriving the Generated Values.

public void testProductPrice_DRVG() {

      // 设置

      产品 product =

            new Product( getUniqueInt(), // ID

                                    getUniqueString("Widget"), // 名称

                                    getUniqueBigDecimal()); // 价格

        // 练习

        // ...

}



static int counter = 0;



public void setUp() {

      counter++;

}



int getUniqueInt() {

      return counter;

}



String getUniqueString(String baseName) {

      return baseName.concat(String.valueOf( getUniqueInt()));

}



BigDecimal getUniqueBigDecimal() {

      return new BigDecimal(getUniqueInt());

}

public  void  testProductPrice_DRVG()  {

      //        Setup

      Product  product  =

            new  Product(  getUniqueInt(),                        //  ID

                                    getUniqueString("Widget"),    //  Name

                                    getUniqueBigDecimal());        //  Price

        //  Exercise

        //      ...

}



static  int  counter  =  0;



public  void  setUp()  {

      counter++;

}



int  getUniqueInt()  {

      return  counter;

}



String  getUniqueString(String  baseName)  {

      return  baseName.concat(String.valueOf(  getUniqueInt()));

}



BigDecimal  getUniqueBigDecimal()  {

      return  new  BigDecimal(getUniqueInt());

}

 

如果我们在对象检查器或数据库中查看该对象,或者将其部分转储到日志中,则无论我们碰巧看到哪个字段,我们都可以很容易地分辨出我们正在查看哪个对象。

If we looked at this object in an object inspector or database or if we dumped part of it to a log, we could readily tell which object we were looking at regardless of which field we happened to see.

虚拟对象

Dummy Object

也称为

Also known as

虚拟、虚拟参数、虚拟值、占位符、桩

Dummy, Dummy Parameter, Dummy Value, Placeholder, Stub

当唯一的用途是作为 SUT 方法调用的不相关参数时,我们如何指定测试中要使用的值?

How do we specify the values to be used in tests when the only usage is as irrelevant arguments of SUT method calls?

我们传递一个没有实现的对象作为在 SUT 上调用的方法的参数。

We pass an object that has no implementation as an argument of a method called on the SUT.

发票 inv = 新发票(新 DummyCustomer());

Invoice  inv  =  new  Invoice(  new  DummyCustomer()  );

要使 SUT 进入正确状态以开始测试,通常需要调用 SUT 的其他方法。这些方法通常将存储在实例变量中以供以后使用的对象作为参数。通常,这些对象(或至少这些对象的某些属性)从未在我们实际测试的代码中使用。相反,我们创建它们只是为了符合我们必须调用的某些方法的签名,以使 SUT 进入正确状态。构造这些对象可能并不简单,并增加了测试不必要的复杂性。

Getting the SUT into the right state to start a test often requires calling other methods of the SUT. These methods commonly take as arguments objects that are stored in instance variables for later use. Often, these objects (or at least some attributes of these objects) are never used in the code that we are actually testing. Instead, we create them solely to conform to the signature of some method we must call to get the SUT into the right state. Constructing these objects can be nontrivial and adds unnecessary complexity to the test.

在这些情况下,可以将虚拟对象作为参数传递,从而无需构建真实对象。

In these cases, a Dummy Object can be passed as an argument, eliminating the need to build a real object.

工作原理

How It Works

我们创建某个对象的实例,该实例可以轻松实例化且没有任何依赖项;然后我们将该实例作为 SUT 方法的参数传递。由于它实际上不会在 SUT 中使用,因此我们不需要为该对象进行任何实现。如果调用Dummy Object的任何方法,测试实际上应该会抛出错误。尝试调用不存在的方法通常会产生该结果。

We create an instance of some object that can be instantiated easily and with no dependencies; we then pass that instance as the argument of the method of the SUT. Because it won't actually be used within the SUT, we don't need any implementation for this object. If any of the methods of the Dummy Object are invoked, the test really should throw an error. Trying to invoke a nonexistent method will typically produce that result.

何时使用它

When to Use It

每当我们需要将对象用作其他对象的属性或 SUT 或其他基架对象上的方法的参数时,我们都可以使用虚拟对象。使用虚拟对象有助于我们避免模糊测试第 186页),因为它可以省略构建真实对象所必需的无关代码,并明确说明 SUT 不使用哪些对象和值。

We can use Dummy Objects whenever we need to use objects as attributes of other objects or arguments of methods on the SUT or other fixture objects. Using Dummy Objects helps us avoid Obscure Tests (page 186) by leaving out the irrelevant code that would be necessary to build real objects and by making it clear which objects and values are not used by the SUT.

如果我们需要控制间接输入或验证 SUT 的间接输出,我们可能应该使用测试桩第 529页)或模拟对象第 544页)。如果对象将被 SUT 使用,但我们无法提供真实对象,则应考虑提供一个伪对象第 551页),该伪对象提供足以执行测试的行为。

If we need to control the indirect inputs or verify the indirect outputs of the SUT, we should probably use a Test Stub (page 529) or a Mock Object (page 544) instead. If the object will be used by the SUT but we cannot provide the real object, we should consider providing a Fake Object (page 551) that provides just enough behavior for the test to execute.

当 SUT 确实需要以某种方式使用对象时,我们可以使用其中一种值模式。根据具体情况,文字值第 714页)、生成值第 723页)或派生值第 718页)都可能适用。

We can use one of the value patterns when the SUT really does need to use the object in some way. Either a Literal Value (page 714), a Generated Value (page 723), or a Derived Value (page 718) may be appropriate, depending on the circumstance.

变体:伪参数

当 SUT 的方法以对象作为参数1并且这些对象与测试无关时,我们就可以使用虚拟参数。

We can use a Dummy Argument whenever methods of the SUT take objects as arguments1 and those objects are not relevant to the test.

变体:虚拟属性

每当我们创建用作装置的一部分或 SUT 方法的参数的对象时,我们都可以使用虚拟属性,并且这些对象的某些属性与测试无关。

We can use a Dummy Attribute whenever we are creating objects that will be used as part of the fixture or as arguments of SUT methods, and some of the attributes of those objects are not relevant to the test.

实施说明

Implementation Notes

虚拟对象最简单的实现是将值作为参数传递null。这种方法即使在 Java 等静态类型语言中也有效,尽管只有在调用的方法不检查空参数的情况下才有效。如果方法在传递时出现问题null,我们将需要采用稍微复杂一些的实现。使用的最大缺点null是它不太具有描述性。

The simplest implementation of a Dummy Object is to pass a null value as the argument. This approach works even in a statically typed language such as Java, albeit only if the method being called doesn't check for null arguments. If the method complains when we pass it null, we'll need to employ a slightly more sophisticated implementation. The biggest disadvantage to using null is that it is not very descriptive.

在动态类型语言(例如 Ruby、Perl 和 Python)中,对象的实际类型永远不会被检查(因为它永远不会被使用),因此我们可以使用任何类,例如String或。在这种情况下,为对象赋予一个自描述值(请参阅文字值Object)很有用,例如“虚拟客户”。

In dynamically typed languages such as Ruby, Perl, and Python, the actual type of the object will never be checked (because it will never be used), so we can use any class such as String or Object. In such a case, it is useful to give the object a Self-Describing Value (see Literal Value) such as "Dummy Customer."

在静态类型语言(如 Java、C# 和 C++)中,我们必须确保虚拟对象与要匹配的参数类型兼容。如果参数具有抽象类型(例如InterfaceJava 中的),则类型兼容性更容易实现,因为我们可以创建自己的类型的简单实现或传递合适的伪对象(请参阅第 568页的硬编码测试替身)。如果参数类型是具体类,我们可能能够创建它的简单实例,或者我们可能需要在测试中创建测试特定子类的实例(第 579页)。

In statically typed languages (such as Java, C#, and C++), we must ensure that the Dummy Object is type compatible with the parameter it is to match. Type compatibility is much easier to achieve if the parameter has an abstract type (e.g., an Interface in Java) because we can create our own trivial implementation of the type or pass a suitable Pseudo-Object (see Hard-Coded Test Double on page 568). If the parameter type is a concrete class, we may be able to create a trivial instance of it or we may need to create an instance of a Test-Specific Subclass (page 579) within our test.

一些Mock 对象框架具有测试实用方法第 599页),它们将为接受自描述值参数的指定类生成虚拟对象String

Some Mock Object frameworks have Test Utility Methods (page 599) that will generate a Dummy Object for a specified class that takes a String argument for a Self-Describing Value.

虽然虚拟对象实际上可能是,但它与空对象[PLOPD3]null不同。SUT不使用虚拟对象,因此其行为要么无关紧要,要么在执行时应抛出异常。相比之下,空对象由 SUT 使用,但设计为不执行任何操作。这是一个很小但非常重要的区别!

While the Dummy Object may, in fact, be null, it is not the same as a Null Object [PLOPD3]. A Dummy Object is not used by the SUT, so its behavior is either irrelevant or it should throw an exception when executed. In contrast, a Null Object is used by the SUT but is designed to do nothing. That's a small but very important distinction!

激励人心的例子

Motivating Example

在这个例子中,我们正在测试,Invoice但我们需要一个Customer来实例化发票。Customer需要一个Address,而这又需要一个City。因此,我们发现自己创建了几个额外的对象只是为了设置夹具。但如果我们知道我们正在测试的行为根本不应该访问Customer,为什么我们需要创建它以及它所依赖的所有对象?

In this example, we are testing the Invoice but we require a Customer to instantiate the invoice. The Customer requires an Address, which in turn requires a City. Thus we find ourselves creating several additional objects just to set up the fixture. But if we know that the behavior we are testing should not access the Customer at all, why do we need to create it and all the objects on which it depends?

public void testInvoice_addLineItem_noECS() {

      final int QUANTITY = 1;

      产品 product = new Product(getUniqueNumberAsString(),

                                                            getUniqueNumber());

      州 state = new State("West Dakota", "WD");

      城市 city = new City("Centreville", state);

      地址 address = new Address("123 Blake St.", city, "12345");

      客户 customer= new Customer(getUniqueNumberAsString(),

                                                                 getUniqueNumberAsString(),

                                                                  address);

      发票 inv = new Invoice(customer);

      // 练习

      inv.addItemQuantity(product, QUANTITY);

      // 验证

      列表 lineItems = inv.getLineItems();

      assertEquals("number of items", lineItems.size(), 1);

      LineItem actual = (LineItem)lineItems.get(0);

      LineItem expItem = new LineItem(inv, product, QUANTITY);

      断言LineItemsEqual(“”,expItem,实际);

}

public  void  testInvoice_addLineItem_noECS()  {

      final  int  QUANTITY  =  1;

      Product  product  =  new  Product(getUniqueNumberAsString(),

                                                            getUniqueNumber());

      State  state  =  new  State("West  Dakota",  "WD");

      City  city  =  new  City("Centreville",  state);

      Address  address  =  new  Address("123  Blake  St.",  city,  "12345");

      Customer  customer=  new  Customer(getUniqueNumberAsString(),

                                                                 getUniqueNumberAsString(),

                                                                  address);

      Invoice  inv  =  new  Invoice(customer);

      //  Exercise

      inv.addItemQuantity(product,  QUANTITY);

      //  Verify

      List  lineItems  =  inv.getLineItems();

      assertEquals("number  of  items",  lineItems.size(),  1);

      LineItem  actual  =  (LineItem)lineItems.get(0);

      LineItem  expItem  =  new  LineItem(inv,  product,  QUANTITY);

      assertLineItemsEqual("",expItem,  actual);

}

 

由于创建了额外的对象,这个测试相当混乱。我们测试的行为与Address和有什么关系City?从这个测试中,我们只能假设存在某种关系。但这会误导测试读者!

This test is quite cluttered as a result of the extra object creation. How is the behavior we are testing related to the Address and City? From this test, we can only assume that there is some relation. But this misleads the test reader!

重构说明

Refactoring Notes

如果夹具中的对象与测试无关,则它们不应该在测试中可见。因此,我们应该尝试消除创建所有这些对象的需要。我们可以尝试传入nullCustomer在这种情况下,构造函数会检查null并拒绝它,所以我们必须找到另一种方法。

If the objects in the fixture are not relevant to the test, they should not be visible in the test. Therefore, we should try to eliminate the need to create all these objects. We could try passing in null for the Customer. In this case, the constructor checks for null and rejects it, so we have to find another way.

解决方案是将测试中不重要的对象替换为虚拟对象。在动态类型语言中,我们只需传入一个字符串即可。但在 Java 和 C# 等静态类型语言中,我们必须传入类型兼容的对象。在本例中,我们选择对 进行提取接口 [Fowler] 重构,Customer以创建一个新接口,然后创建一个名为 的新实现类DummyCustomer。当然,作为提取接口重构的一部分,我们必须Customer用新接口名称替换对 的所有引用,以便DummyCustomer可以被接受。一个干扰较小的选项是使用 的测试特定子类,Customer添加了一个测试友好的构造函数。

The solution is to replace the object that is not important to our test with a Dummy Object. In dynamically typed languages, we could just pass in a string. In statically typed languages such as Java and C#, however, we must pass in a type-compatible object. In this case, we have chosen to do an Extract Interface [Fowler] refactoring on Customer to create a new interface and then create a new implementation class called DummyCustomer. Of course, as part of the Extract Interface refactoring, we must replace all references to Customer with the new interface name so that the DummyCustomer will be acceptable. A less intrusive option would be to use a Test-Specific Subclass of Customer that adds a test-friendly constructor.

示例:虚拟值和虚拟对象

Example: Dummy Values and Dummy Objects

这是使用虚拟对象代替Product名称和 的相同测试Customer。请注意,夹具设置变得多么简单!

Here's the same test using a Dummy Object instead of the Product name and the Customer. Note how much simpler the fixture setup has become!

  public void testInvoice_addLineItem_DO() {

        final int QUANTITY = 1;

        Product product = new Product("虚拟产品名称",

                                                             getUniqueNumber());

        Invoice inv = new Invoice( new DummyCustomer() );

        LineItem expItem = new LineItem(inv, product, QUANTITY);

        // 练习

        inv.addItemQuantity(product, QUANTITY);

        // 验证

        列表 lineItems = inv.getLineItems();

        assertEquals("项目数量", lineItems.size(), 1);

        LineItem actual = (LineItem)lineItems.get(0);

        assertLineItemsEqual("", expItem, actual);

}

  public  void  testInvoice_addLineItem_DO()  {

        final  int  QUANTITY  =  1;

        Product  product  =  new  Product("Dummy  Product  Name",

                                                             getUniqueNumber());

        Invoice  inv  =  new  Invoice(  new  DummyCustomer()  );

        LineItem  expItem  =  new  LineItem(inv,  product,  QUANTITY);

        //  Exercise

        inv.addItemQuantity(product,  QUANTITY);

        //  Verify

        List  lineItems  =  inv.getLineItems();

        assertEquals("number  of  items",  lineItems.size(),  1);

        LineItem  actual  =  (LineItem)lineItems.get(0);

        assertLineItemsEqual("",  expItem,  actual);

}

 

使用虚拟对象作为 的名称Product很简单,因为它是一个字符串并且没有唯一性要求。因此,我们能够使用自描述值。我们不能使用虚拟对象作为Product数字,因为它必须是唯一的,所以我们将其保留为生成的值Customer有点棘手,因为LineItem的构造函数需要一个非空对象。由于此示例是用 Java 编写的,所以方法参数是强类型的;因此,我们需要创建接口的替代实现,ICustomer使用无参数构造函数来简化内联构造。因为DummyCustomer从未使用过,所以我们内联创建了它,而不是声明一个变量来保存它。这种选择将夹具设置代码减少了一行,并且在构造函数调用内存在内联构造函数调用,这Invoice强化了这样一个信息:我们只需要在构造函数调用中使用虚拟对象,而不需要其余的测试。

Using a Dummy Object for the name of the Product was simple because it is a string and has no uniqueness requirement. Thus we were able to use a Self-Describing Value. We were not able to use a Dummy Object for the Product number because it must be unique, so we left it as a Generated Value. The Customer was a bit trickier because the LineItem's constructor expected a non-null object. Because this example is written in Java, the method parameter is strongly typed; for this reason, we needed to create an alternative implementation of the ICustomer interface with a no-argument constructor to simplify in-line construction. Because the DummyCustomer is never used, we have created it in-line rather than declaring a variable to hold it. This choice reduces the fixture setup code by one line, and the presence of the in-line constructor call within the call to the Invoice constructor reinforces the message that we need the Dummy Object only for the constructor call and not for the rest of the test.

以下是的代码DummyCustomer

Here is the code for the DummyCustomer:

  public class DummyCustomer implements ICustomer {



        public DummyCustomer() {

              // 非常简单;无需初始化!

        }



        public int getZone() {

              throw new RuntimeException("This should never be called!");

        }

}

  public  class  DummyCustomer  implements  ICustomer  {



        public  DummyCustomer()  {

              //  Real  simple;  nothing  to  initialize!

        }



        public  int  getZone()  {

              throw  new  RuntimeException("This  should  never  be  called!");

        }

}

 

我们仅使用接口中声明的那些方法实现了该类DummyCustomer;因为每个方法都会引发异常,所以我们知道何时会触发异常。我们还可以为 使用了伪对象DummyCustomer。在其他情况下,我们可能能够简单地传入null或构造真实类的虚拟实例。后一种技术的主要问题是我们无法确定虚拟对象是否真的被使用。

We have implemented the DummyCustomer class with just those methods declared in the interface; because each method throws an exception, we know when it is hit. We could also have used a Pseudo-Object for the DummyCustomer. In other circumstances we might have been able to simply pass in null or construct a dummy instance of the real class. The major problem with the latter technique is that we won't know for sure if the Dummy Object is actually used.

进一步阅读

[UTwJ]提到“虚拟对象”时,这些作者指的是本书中所说的测试桩。请参阅附录 B中的模拟、伪造、桩和虚拟对象,以更全面地比较各种书籍和文章中使用的术语。使用模拟对象进行测试的 JMock 和 NMock 框架支持自动生成虚拟对象

When [UTwJ] refers to a "dummy object," these authors are referring to what this book terms a Test Stub. See Mocks, Fakes, Stubs, and Dummies in Appendix B for a more thorough comparison of the terminology used in various books and articles. The JMock and NMock frameworks for testing with Mock Objects support auto-generation of Dummy Objects.

第四部分

附录

Part IV

Appendixes

 

附录 A

测试重构

Appendix A

Test Refactorings

 

提取可测试组件

Extract Testable Component

也称为

Also known as

萌芽班[WEwLC]

Sprout Class [WEwLC]

您希望能够轻松地测试逻辑,但是组件与其上下文联系过于紧密,以至于无法进行这样的测试。

You want to be able to test the logic easily but the component is too closely tied to its context to allow such testing.

将您想要测试的逻辑提取到一个单独的组件中,该组件专为可测试性而设计,并且独立于其运行的上下文。

Extract the logic you want to test into a separate component that is designed for testability and is independent of the context in which it is run.

实施说明

Implementation Notes

我们将不可测试组件中的逻辑提取到可通过同步测试进行测试的组件中,同时保留与上下文的所有关联。这通常意味着,可测试组件逻辑从上下文中获取的任何内容都由不可测试组件检索,并作为测试方法或构造函数方法的参数传递给可测试组件。然后,不可测试组件包含的代码非常少,被视为一个Humble 对象第 695页)。它只是从上下文中获取可测试组件所需的信息,实例化可测试组件,并委托给它。与新可测试组件的所有交互都由同步方法调用组成。

We extract the logic from the untestable component into a component that is testable via synchronous tests, leaving behind all the ties to the context. This usually means that anything required by the testable component logic from the context is retrieved by the untestable component and passed in to the testable component as arguments of the methods under test or constructor methods. The untestable component then contains very little code and is considered to be a Humble Object (page 695). It simply retrieves the information the testable component requires from the context, instantiates the testable component, and delegates to it. All interactions with the new testable component consist of synchronous method calls.

可测试组件可能是 Windows DLL、包含 Service Facade [CJ2EEP]类的 Java JAR,或者以可测试方式公开可执行文件服务的其他语言组件或类。不可测试代码可能是可执行文件、对话框或其他表示组件、在事务内部执行的逻辑,甚至是一种复杂的测试方法。可测试组件的提取应该留下一个几乎不需要测试的Humble 对象。

The testable component may be a Windows DLL, a Java JAR containing a Service Facade [CJ2EEP] class, or some other language component or class that exposes the services of the executable in a testable way. The untestable code may be an executable, a dialog box or some other presentation component, logic that is executed inside a transaction, or even a complex test method. Extraction of the testable component should leave behind a Humble Object that requires very little, if any, testing.

根据不可测试组件的性质,我们可能会选择为委托逻辑编写测试,或者我们可能无法这样做,因为逻辑与上下文紧密相关。如果我们确实为其编写测试,我们只需要一两个测试来验证实例化和委托是否正确发生。由于此代码不会经常更改,因此这些测试的重要性远低于其他测试,如果我们想加快测试套件的执行时间,甚至可以从开发人员在签入之前执行的测试套件中省略它们。当然,我们仍然希望从自动构建过程中运行它们。

Depending on the nature of the untestable component, we may choose to write tests for the delegation logic or we may be unable to do so because the logic is so closely tied to the context. If we do write tests for it, we require only one or two tests to verify that the instantiation and delegation occur correctly. Because this code will not change very often, these tests are much less critical than other tests and can even be omitted from the suite of tests that developers execute before check-in if we want to speed up test suite execution times. Of course, we would still prefer to run them from the automated build process.

进一步阅读

此重构类似于提取接口 [Fowler] 重构和提取实现者 [Fowler] 重构,不同之处在于提取可测试组件不需要保留相同的接口。它也可以看作提取类 [Fowler] 重构的一个特例。

This refactoring is similar to an Extract Interface [Fowler] refactoring and an Extract Implementer [Fowler] refactoring, except that Extract Testable Component does not require keeping the same interface. It can also be viewed as a special case of the Extract Class [Fowler] refactoring.

内联资源

In-line Resource

依赖于看不见的外部资源的测试会产生神秘客人问题。

Tests that depend on an unseen external resource create a Mystery Guest problem.

将外部资源的内容移动到测试的装置设置逻辑中。

Move the contents of an external resource into the fixture setup logic of the test.

来自[RTC]:

From [RTC]:

为了消除测试方法与某些外部资源之间的依赖关系,我们将该资源合并到测试代码中。这是通过在测试代码中设置一个包含与资源相同内容的装置来实现的。然后使用此装置代替资源来运行测试。这种重构的一个简单示例是将使用的文件的内容放入测试代码中的某个字符串中。

To remove the dependency between a test method and some external resource, we incorporate that resource in the test code. This is done by setting up a fixture in the test code that holds the same contents as the resource. This fixture is then used instead of the resource to run the test. A simple example of this refactoring is putting the contents of a file that is used into some string in the test code.

 

如果资源内容很大,则很有可能您也遭受了 Eager 测试的困扰(请参阅第 224页的断言轮盘赌)。考虑应用提取方法 [Fowler] 重构或最小化数据第 738页)重构。

If the contents of the resource are large, chances are high that you are also suffering from Eager Tests (see Assertion Roulette on page 224). Consider applying an Extract Method [Fowler] refactoring or a Minimize Data (page 738) refactoring.

 

实施说明

Implementation Notes

依赖于外部资源的测试的问题在于我们无法看到测试的先决条件。资源可能是文件系统中的文件、数据库的内容或在测试之外创建的其他对象。这些预建夹具第 429页)对测试读取者都是不可见的。解决方案是通过在测试内联包含资源使它们可见。最简单的方法是在测试内部创建资源。例如,我们可以通过写入文件而不是仅仅引用预先存在的文件来构建文本文件的内容。如果我们在测试结束时删除文件,这一步骤也会将我们从预建夹具方法转变为持久新夹具(请参阅第311页的新夹具)方法。因此,我们的测试执行速度可能会稍微慢一些。

The problem with tests that depend on an external resource is that we cannot see the pre-conditions of the test. The resource may be a file sitting in the file system, the contents of a database, or some other object created outside the test. None of these Prebuilt Fixtures (page 429) is visible to the test reader. The solution is to make them visible by including the resource in-line within the test. The simplest way to do so is to create the resource from within the test itself. For example, we could build the contents of a text file by writing to the file rather than just referring to a preexisting file. If we delete the file at the end of the test, this step also moves us from a Prebuilt Fixture approach to a Persistent Fresh Fixture (see Fresh Fixture on page 311) approach. As a result, our tests may execute somewhat more slowly.

内联外部资源的一种更具创新性的方式是用在测试中初始化的测试桩(第 529页) 替换实际资源。资源的内容随后对测试读取器可见。当被测系统 (SUT) 执行时,它使用测试桩而不是实际资源。

A more innovative way to in-line the external resource is to replace the actual resource with a Test Stub (page 529) that is initialized within the test. The contents of the resource then become visible to the test reader. When the system under test (SUT) executes, it uses the Test Stub instead of the real resource.

另一个选择是重构 SUT 的设计,以提高其可测试性。我们可以将提取可测试组件 (第735页) 重构应用于使用资源内容的 SUT 部分,以便可以直接对其进行测试,而无需实际访问外部资源。也就是说,测试将资源的内容传递给使用它的逻辑。我们还可以通过用测试桩模拟对象(第 544页)替换提取的组件来测试独立读取资源的Humble 对象(第695页)。

Another option is to refactor the design of the SUT so as to improve its testability. We can apply the Extract Testable Component (page 735) refactoring to the part of the SUT that uses the contents of the resource so that it can be tested directly without actually accessing an external resource. That is, the test passes the contents of the resource to the logic that uses it. We can also test the Humble Object (page 695) that reads the resource independently by replacing the extracted component with a Test Stub or Mock Object (page 544).

使资源唯一

Make Resource Unique

多个测试在共享装置中意外创建或使用了相同的资源

Several tests are accidentally creating or using the same resource in a Shared Fixture.

使测试使用的任何资源的名称都是唯一的。

Make the name of any resources used by a test unique.

来自[RTC]:

From [RTC]:

许多问题源于使用重叠的资源名称,无论是在同一用户运行的不同测试之间,还是在不同用户同时进行的测试运行之间。

A lot of problems originate from the use of overlapping resource names, either between different tests run by the same user or between simultaneous test runs done by different users.

 

通过使用所有分配的资源的唯一标识符(例如,通过包含时间戳),可以轻松防止(或修复)此类问题。如果您还在此标识符中包含负责分配资源的测试的名称,那么找到未正确释放其资源的测试时就会遇到更少的问题。

Such problems can easily be prevented (or repaired) by using unique identifiers for all resources that are allocated—for example, by including a time stamp. When you also include the name of the test responsible for allocating the resource in this identifier, you will have fewer problems finding tests that do not properly release their resources.

 

实施说明

Implementation Notes

我们使用独特的生成值(请参阅第723页的生成值)作为名称的一部分,使测试使用的任何资源的名称在所有测试中都是唯一的。理想情况下,名称应包括“拥有”该资源的测试的名称。为了避免交互测试(请参阅第228页的不稳定测试),我们在测试创建的任何资源的名称中包含时间戳,并使用自动拆卸(第503页)在测试结束时删除这些资源。

We make the name of any resources used by a test unique across all tests by using a Distinct Generated Value (see Generated Value on page 723) as part of the name. Ideally, the name should include the name of the test that "owns" the resource. To avoid Interacting Tests (see Erratic Test on page 228), we include a time stamp in the name of any resources created by the tests and use Automated Teardown (page 503) to delete those resources at the end of the test.

最小化数据

Minimize Data

也称为

Also known as

减少数据

Reduce Data

测试夹具太大,导致测试难以理解。

The test fixture is too large, making the test hard to understand.

我们从固定装置中移除东西,直到我们得到一个最小固定装置。

We remove things from the fixture until we have a Minimal Fixture.

来自[RTC]:

From [RTC]:

将装置中设置的数据最小化到最基本的必需品。这将有两个好处:(1)它使它们更适合作为文档,(2)您的测试对变化不太敏感。

Minimize the data that is set up in fixtures to the bare essentials. This will have two advantages: (1) It makes them more suitable as documentation, and (2) your tests will be less sensitive to changes.

 

实施说明

Implementation Notes

将测试装置中的数据减少到最低限度,可产生一个最小装置(第 302页),帮助测试实现“测试即文档” (参见第23页)。如何做到这一点取决于我们的测试方法(第 348页) 如何组织到测试用例类(第 373页)。

Reducing the data in our test fixture to the bare minimum results in a Minimal Fixture (page 302) that helps the tests achieve Tests as Documentation (see page 23). How we do this depends on how our Test Methods (page 348) are organized into Testcase Classes (page 373).

当我们的测试方法按照每个装置一个测试用例类的模式进行组织时(第631页),并且我们相信我们有一个通用装置(请参阅第186页的模糊测试),我们可以删除装置中我们怀疑测试未使用的任何部分的装置设置逻辑。最好逐步删除此逻辑,以便如果测试失败,我们可以撤消最近的更改并重试。

When our Test Methods are organized via the Testcase Class per Fixture pattern (page 631) and we believe we have a General Fixture (see Obscure Test on page 186), we can remove the fixture setup logic for any parts of the fixture that we suspect are not used by the tests. It is best to remove this logic incrementally so that if a test fails, we can undo our most recent change and try again.

当我们的测试方法被组织为每个功能一个测试用例类(第624页) 或每个类一个测试用例类(第617页) 时,最小化数据可能还涉及将夹具设置逻辑从测试用例类设置装饰器(第447setUp页)的方法复制到每个需要夹具的测试中。假设共享夹具(第 317 页)中的对象集合对于任何一个测试来说都是多余的,我们可以使用一系列提取方法 [Fowler] 重构来创建一组创建方法(第 415页),然后在测试中调用它们。接下来,我们从方法中删除对创建方法的调用,并将它们放入仅需要原始夹具那部分的测试方法中。最后一步是将任何保存夹具的实例变量转换为局部变量。setUp

When our Test Methods are organized as a Testcase Class per Feature (page 624) or a Testcase Class per Class (page 617), Minimize Data may also involve copying fixture setup logic from the setUp method of a Testcase Class or Setup Decorator (page 447) into each test that needs the fixture. Assuming the collection of objects in the Shared Fixture (page 317) is overkill for any one test, we can use a series of Extract Method [Fowler] refactorings to create a set of Creation Methods (page 415), which we then call from the tests. Next, we remove the calls to the Creation Methods from the setUp method and put them into only those Test Methods that require that part of the original fixture. The final step would be to convert any fixture-holding instance variables into local variables.

用测试替身替代依赖

Replace Dependency with Test Double

被测试对象的依赖关系妨碍了测试的运行。

The dependencies of an object being tested get in the way of running tests.

通过用测试替身替换所依赖的组件来打破依赖关系。

Break the dependency by replacing a depended-on component with a Test Double.

实施说明

Implementation Notes

第一步是选择依赖替换的形式。依赖注入第 678页)是单元测试的最佳选择,而依赖查找(第686页)通常更适合客户测试。然后,我们重构 SUT 以支持这种选择,或者在进行测试驱动开发时将此功能设计到 SUT 中。下一个决定是根据测试将如何使用测试替身来决定是使用伪对象(第551页)、测试桩(第 529页)、测试间谍(第538页)还是模拟对象第 544页) 。第 11 章使用测试替身”描述了这个决定。

The first step is to choose the form of dependency substitution. Dependency Injection (page 678) is the best option for unit tests, whereas Dependency Lookup (page 686) often works better for customer tests. We then refactor the SUT to support this choice or design the capability into the SUT as we do test-driven development. The next decision is whether to use a Fake Object (page 551), a Test Stub (page 529), a Test Spy (page 538), or a Mock Object (page 544) based on how the Test Double will be used by the test. This decision is described in Chapter 11, Using Test Doubles.

如果我们使用的是测试桩模拟对象,则必须决定是使用硬编码测试替身第 568页)还是可配置测试替身(第558页)。第 11 章和模式的详细描述中讨论了权衡。然后,该决定决定了我们测试的形状 — 例如,使用模拟对象的测试更多地通过构造模拟对象来“前置” 。

If we are using a Test Stub or Mock Object, we must decide whether we want to use a Hard-Coded Test Double (page 568) or a Configurable Test Double (page 558). The trade-offs are discussed in Chapter 11 and in the detailed descriptions of the patterns. That decision then dictates the shape of our test—for example, Tests that use Mock Objects are more "front-loaded" by the construction of the Mock Object.

最后,我们修改测试以构造、配置(可选)然后安装Mock Objectverification 。对于某些类型的Mock Object ,我们可能还必须添加对方法的调用。在静态类型语言中,我们可能必须先进行 Extract Interface [Fowler] 重构,然后才能引入伪实现。然后,我们将此接口用作保存对可替代依赖项的引用的变量的类型。

Finally, we modify our test to construct, optionally configure, and then install the Mock Object. We may also have to add a call to the verification method for some kinds of Mock Objects. In statically typed languages, we may have to do an Extract Interface [Fowler] refactoring before we can introduce the fake implementation. We then use this interface as the type of the variable that holds the reference to the substitutable dependency.

设置外部资源

Setup External Resource

SUT 依赖于在我们的测试中充当神秘嘉宾的外部资源的内容。

The SUT depends on the contents of an external resource that is acting as a Mystery Guest in our test.

在测试的装置设置逻辑中创建外部资源,而不是使用预定义资源。

Create an external resource within the fixture setup logic of the test rather than using a predefined resource.

来自[RTC]

From [RTC]:

如果测试需要依赖外部资源,例如目录、数据库或文件,请确保使用它们的测试在测试之前明确创建或分配这些资源,并在完成后释放它们(采取预防措施确保测试失败时也释放资源)。

If it is necessary for a test to rely on external resources, such as directories, databases, or files, make sure the test that uses them explicitly creates or allocates these resources before testing, and releases them when done (take precautions to ensure the resource is also released when tests fail).

 

实施说明

Implementation Notes

当我们的 SUT必须使用外部资源(比如文件)而我们绝对、肯定不能用测试桩第 529页)或伪对象第 551页)取代访问机制时,我们可能需要接受必须使用外部资源的现实。外部资源的问题很明显:测试阅读者无法分辨它们包含什么;这些资源可能会意外消失,导致测试因资源乐观主义而失败(请参阅第 228页的不稳定的测试);并且这些资源可能会导致交互测试(请参阅不稳定的测试)和测试运行战争(请参阅不稳定的测试)。设置外部资源并不能帮助我们解决最后一个难题,但它确实避免了神秘客人(请参阅第 186页的模糊测试)和资源乐观主义的问题。

When our SUT must use an external resource such as a file and we absolutely, positively cannot replace the access mechanism with a Test Stub (page 529) or Fake Object (page 551), we may need to live with the fact that we have to use an external resource. The problems with external resources are obvious: The test reader cannot tell what they contain; those resources may disappear unexpectedly, causing tests to fail because of Resource Optimism (see Erratic Test on page 228); and the resources may result in Interacting Tests (see Erratic Test) and Test Run Wars (see Erratic Test). Setup External Resource does not help us with the last problem but it does avoid the problems of a Mystery Guest (see Obscure Test on page 186) and Resource Optimism.

要实现“设置外部资源”重构,我们只需将外部资源的内容拉入我们的测试方法(第 348页)、setUp方法或它们调用的测试实用程序方法(第599页) 中。使用这些内容,我们在测试代码中构建外部资源,从而让测试读者清楚地知道测试所依赖的内容。这种方法还可以保证资源存在,因为我们在每次测试运行中都会创建它。

To implement the Setup External Resource refactoring, we simply pull the contents of the external resource into our Test Method (page 348), setUp method, or a Test Utility Method (page 599) called by them. Using the contents we construct the external resource within our test code, thereby making it evident to the test reader exactly what the test depends on. This approach also guarantees that the resource exists because we create it in every test run.

附录 B

xUnit 术语

Appendix B

xUnit Terminology

 

模拟、伪造、桩和假人

Mocks, Fakes, Stubs, and Dummies

当某人说“测试桩”或“模拟对象”时,您是否对某人的意思感到困惑?您是否有时觉得与您交谈的人使用的定义非常不同?好吧,您并不孤单!

Are you confused about what someone means when that individual says "test stub" or "mock object"? Do you sometimes feel that the person you are talking to is using a very different definition? Well, you are not alone!

各种测试替身第 522页)的术语令人困惑且不一致。不同的作者使用不同的术语来表示同一件事。有时,即使他们使用相同的术语,他们的意思也不同!哎哟!(请参阅第576页的侧栏“ (模式)名称包含什么? ”,了解我为什么认为名称很重要。)

The terminology for the various kinds of Test Doubles (page 522) is confusing and inconsistent. Different authors use different terms to mean the same thing. And sometimes they mean different things even when they use the same term! Ouch! (See the sidebar "What's in a (Pattern) Name?" on page 576 for why I think names are important.)

我写这本书的部分原因是试图在术语上建立一定的一致性,从而为人们提供一组具有明确含义的名称。在本附录中,我提供了当前来源的列表,并将它们使用的术语与本书中使用的术语进行了交叉引用。

Part of my reason for writing this book was to try to establish some consistency in the terminology, thereby giving people a set of names with clear definitions of what they mean. In this appendix, I provide a list of the current sources and cross-reference the terminology they use with the terminology used in this book.

角色描述

Role Descriptions

第 742页的表格是对每个主要测试双重模式名称的含义的总结。

The table on page 742 is a summary of what I mean by each of the major Test Double pattern names.

图像

术语交叉引用

Terminology Cross-Reference

下表列出了一些相互冲突的定义来源,只是为了清楚地说明本书中使用的模式名称的映射。

The following table lists some sources of conflicting definitions just to make it clear what the mapping is to the pattern names used in this book.

图像

  • Java 单元测试 [UTwJ]使用术语“虚拟对象”来指代本书所称的“伪对象”。
  • Unit Testing with Java [UTwJ] uses the term "Dummy Object" to refer to what this book calls a "Fake Object."
  • 实用单元测试 [PUT]将“桩”描述为方法的空实现。这是程序世界中的常见解释;然而,在对象世界中,它通常被称为空对象[PLOPD3]
  • Pragmatic Unit Testing [PUT] describes a "Stub" as an empty implementation of a method. This is a common interpretation in the procedural world; in the object world, however, it is typically called a Null Object [PLOPD3].
  • 一些早期的 Mock Objects 文献可能会被解释为将“Stub”等同于“Mock Object”。两者之间的区别已在[MRNO][MAS]中得到澄清。
  • Some of the early Mock Objects literature could be interpreted to equate a "Stub" with a "Mock Object." The distinction between the two has since been clarified in [MRNO] and [MAS].
  • CORBA 标准1和其他远程过程调用规范使用术语“桩”和“骨架”来指代 IDL 中定义的远程接口的近端和远端实现的自动生成代码。(我在这里包含此信息是因为它是 TDD 和自动化开发人员测试社区中常用术语的另一种用法。)
  • The CORBA standard 1 and other remote-procedure call specifications use the terms "stubs" and "skeletons" to refer to the automatically generated code for the near- and far-end implementations of a remote interface defined in IDL. (I've included this information here because it is another use of a term that is commonly used in the TDD and automated developer testing community.)

上表所引资料来源如下:

The sources quoted in the preceding table are provided here:

图像

xUnit 术语交叉引用

xUnit Terminology Cross-Reference

下表将本书中使用的术语与 xUnit 系列特定成员使用的术语进行了映射。此列表并非详尽无遗,而是为了说明标准 xUnit 术语如何适应每种语言和社区的习语和文化。

The following table maps the terminology used in this book to the terminology used by specific members of the xUnit family. This list is not intended to be exhaustive but rather is meant to illustrate the adaptations of the standard xUnit terminology to the idioms and culture of each language and community.

图像

图像

附录 C

xUnit 家族成员

Appendix C

xUnit Family Members

 

这里列出了 xUnit 系列测试自动化框架的成员(不完整),以说明该系列的多样性以及各种编程语言对自动化单元测试的支持程度。本附录还包含有关该系列某些成员的特定功能的评论。可以在http://xprogramming.com/software.htm找到更完整、最新的列表。

This (incomplete) list of members of the xUnit family of test automation frameworks is included here to illustrate the diversity of the family and the extent to which automated unit testing is supported in various programming languages. This appendix also includes comments about specific capabilities of some members of the family. A much more complete and up-to-date list can be found at http://xprogramming.com/software.htm.

ABAP 对象单元

ABAP Object Unit

SAP ABAP 编程语言 xUnit 系列的成员。ABAP Object Unit或多或少是 JUnit 到 ABAP 的直接移植,但它无法捕获被测系统 (SUT) 中遇到的异常。

The member of the xUnit family for SAP's ABAP programming language. ABAP Object Unit is more or less a direct port of JUnit to ABAP except for the fact that it cannot catch exceptions encountered within the system under test (SUT).

ABAP Object Unit可从http://www.abapunittests.com下载,同时还可查看有关 ABAP 单元测试的文章。有关 SAP/ABAP 6.40 及以上版本的信息,请参阅 ABAP Unit。

ABAP Object Unit is available for download at http://www.abapunittests.com, along with articles about unit testing in ABAP. See ABAP Unit for versions of SAP/ABAP starting with 6.40.

ABAP 单元

ABAP Unit

xUnit 系列的成员,适用于从 Basis 版本 6.40(NetWeaver 2004s)开始的 SAP ABAP 编程语言版本。ABAP Unit 最值得注意的方面是它的特殊支持,允许在将代码从验收测试环境“传输”到生产环境时从代码中剥离测试。

The member of the xUnit family for versions of SAP's ABAP programming language starting with Basis version 6.40 (NetWeaver 2004s). The most notable aspect of ABAP Unit is its special support that allows tests to be stripped from the code as the code is "transported" from the acceptance test environment to the production environment.

ABAP Unit可直接从 SAP AG 获取,作为 NetWeaver 2004s 开发工具的一部分。有关 ABAP 单元测试的更多信息,请参阅 SAP 文档和 http://www.abapunittests.com有关Basis 版本 6.40(NetWeaver 2004s)之前的 SAP/ABAP 版本,请参阅ABAP Object Unit。

ABAP Unit is available directly from SAP AG as part of the NetWeaver 2004s development tools. More information on unit testing in ABAP is available in the SAP documentation and from http://www.abapunittests.com. See ABAP Object Unit for versions of SAP/ABAP prior to Basis version 6.40 (NetWeaver 2004s).

单元计算单元

CppUnit

C++ 编程语言 xUnit 家族的成员。可从http://cppunit.sourceforge.net下载。对于某些 .NET 程序员来说,另一个选择是 NUnit。

The member of the xUnit family for the C++ programming language. It is available for download from http://cppunit.sourceforge.net. Another option for some .NET programmers is NUnit.

陣容

CsUnit

C# 编程语言 xUnit 系列的成员。可从http://www.csunit.org获取。.NET 程序员的另一个选择是 NUnit。

The member of the xUnit family for the C# programming language. It is available from http://www.csunit.org. Another option for .NET programmers is NUnit.

中央单位

CUnit

C 编程语言 xUnit 系列的成员。详细信息可在http://cunit.sourceforge.net/doc/index.html找到。

The member of the xUnit family for the C programming language. Details can be found at http://cunit.sourceforge.net/doc/index.html.

数据库单元

DbUnit

JUnit 框架的扩展,旨在简化数据库测试。可从http://www.dbunit.org/下载。

An extension of the JUnit framework intended to simplify testing of databases. It can be downloaded from http://www.dbunit.org/.

单元

IeUnit

xUnit 系列的成员之一,用于测试使用 JavaScript 和 DHTML 在 Microsoft Internet Explorer 浏览器中呈现的网页。可从http://ieunit.sourceforge.net/下载。

The member of the xUnit family for testing Web pages rendered in Microsoft's Internet Explorer browser using JavaScript and DHTML. It can be downloaded from http://ieunit.sourceforge.net/.

行为行为

JBehave

新一代 xUnit 成员中的第一批成员之一,旨在使作为 TDD 的一部分编写的测试更有用测试即规范。 JBehave与 xUnit 家族中更传统的成员之间的主要区别在于, JBehave避开了“测试”术语,而是用更适合规范的术语取而代之 — 即,“fixture”变成了“context”,“assert”变成了“should”,等等。JBehave可在http://jbehave.codehaus.org上找到。 RSpec 是 Ruby 的等价物。

One of the first of a new generation of xUnit members designed to make tests written as part of TDD more useful Tests as Specification. The main difference between JBehave and more traditional members of the xUnit family is that JBehave eschews the "test" terminology and replaces it with terms more appropriate for specification—that is, "fixture" becomes "context," "assert" becomes "should," and so on. JBehave is available at http://jbehave.codehaus.org. RSpec is the Ruby equivalent.

JUnit

JUnit

Java 编程语言 xUnit 系列的成员。JUnit2005 年底重写,以利用 Java 1.5 中引入的注释。可以从http://www.junit.org下载。

The member of the xUnit family for the Java programming language. JUnit was rewritten in late 2005 to take advantage of the annotations introduced in Java 1.5. It can be downloaded from http://www.junit.org.

兆字节单位

MbUnit

C# 编程语言的 xUnit 家族成员。MbUnit出名的地方在于它直接支持参数化测试。可从http://www.nunit.orgmbunit.com获取。.NET 程序员的其他选择包括 NUnit、CsUnit 和 MSTest。

The xUnit family member for the C# programming language. MbUnit's main claim to fame is its direct support for Parameterized Tests. It is available from http://www.nunit.orgmbunit.com. Other options for .NET programmers include NUnit, CsUnit, and MSTest.

测试

MSTest

微软的 xUnit 家族成员似乎除了其命名空间之外没有正式名称,Microsoft.VisualStudio.TestTools.UnitTesting但大多数人都将其称为MSTest 。从技术上讲,它只是命令行测试运行器 的名称mstest.exe。MSTest出名的地方在于它随 Visual Studio 2005 Team System 一起提供。它似乎没有在较便宜的 Visual Studio 版本中提供或免费下载。MSTest包含许多创新功能,例如对数据驱动测试的直接支持。有关信息可在 MSDN 上找到,网址为http://msdn.microsoft.com/en-us/library/ms182516.aspx。.NET 程序员的其他(更便宜的)选项包括 NUnit、CsUnit 和 MbUnit。

Microsoft's member of xUnit family does not seem to have a formal name other than its namespace Microsoft.VisualStudio.TestTools.UnitTesting but most people refer to it as MSTest. Technically, it is just the name of the Command-Line Test Runner mstest.exe. MSTest's main claim to fame is that it ships with Visual Studio 2005 Team System. It does not appear to be available in the less expensive versions of Visual Studio or for free download. MSTest includes a number of innovative features, such as direct support for Data-Driven Tests. Information is available on MSDN at http://msdn.microsoft.com/en-us/library/ms182516.aspx. Other (and cheaper) options for .NET programmers include NUnit, CsUnit, and MbUnit.

单元

NUnit

.NET 编程语言 xUnit 系列的成员。可从http://www.nunit.org获取。C# 程序员的其他选择包括 CsUnit、MbUnit 和 MSTest。

The member of the xUnit family for the .NET programming languages. It is available from http://www.nunit.org. Other options for C# programmers include CsUnit, MbUnit, and MSTest.

PHPUnit

PHPUnit

PHP 编程语言 xUnit 家族的成员。Sebastian Bergmann 表示:“ PHPUnit是 JUnit 3.8 的完整移植。除了这套原始功能外,它还添加了对模拟对象、代码覆盖率、敏捷文档以及不完整和跳过的测试的开箱即用支持。”有关PHPUnit的更多信息,请访问http://www.phpunit.de,其中包括有关PHPUnit的免费书籍。

The member of the xUnit family for the PHP programming language. According to Sebastian Bergmann, "PHPUnit is a complete port of JUnit 3.8. On top of this original feature set it adds out-of-the-box support for Mock Objects, Code Coverage, Agile Documentation, and Incomplete and Skipped Tests." More information about PHPUnit can be found at http://www.phpunit.de, including the free book on PHPUnit.

单元测试

PyUnit

xUnit 家族的成员,旨在支持 Python 程序员。它是 JUnit 的完整移植。更多信息可在http://pyunit.sourceforge.net/找到。

The member of the xUnit family written to support Python programmers. It is a full port of JUnit. More information can be found at http://pyunit.sourceforge.net/.

规范

RSpec

新一代 xUnit 成员中的第一批成员之一,旨在使作为 TDD 一部分编写的测试更有用测试即规范。 RSpec与 xUnit 家族中更传统的成员之间的主要区别在于, RSpec避开了“测试”术语,而是用更适合规范的术语取而代之 — 例如,“fixture”变成了“context”,测试方法变成了“specify”,“assert”变成了“should”,等等。RSpec可在http://rspec.rubyforge.org获得。 JBehave 是 Java 的等价物。

One of the first of a new generation of xUnit members designed to make tests written as part of TDD more useful Tests as Specification. The main difference between RSpec and more traditional members of the xUnit family is that RSpec eschews the "test" terminology and replaces it with terms more appropriate for specification—for example, "fixture" becomes "context," Test Methods becomes "specify," "assert" becomes "should," and so on. RSpec is available at http://rspec.rubyforge.org. JBehave is the Java equivalent.

鲁尼特

runit

Ruby 编程语言 xUnit 系列的一个成员。它是 Test::Unit 的包装器,添加了附加功能。可在www.rubypeople.org上找到。

One member of the xUnit family for the Ruby programming language. It is a wrapper on Test::Unit that adds additional functionality. It is available at www.rubypeople.org.

太阳单位

SUnit

自称为“所有单元测试框架之母”。SUnit是 Smalltalk 编程语言xUnit系列的成员。可在http://sunit.sourceforge.net下载。

The self-proclaimed "mother of all unit-testing frameworks." SUnit is the member of the xUnit family for the Smalltalk programming language. It is available for download at http://sunit.sourceforge.net.

测试::单元

Test::Unit

Ruby 编程语言 xUnit 家族的成员。可从http://www.rubypeople.org下载,是 Eclipse IDE 框架的“Ruby 开发工具”功能的一部分。

The member of the xUnit family for the Ruby programming language. It is available for download from http://www.rubypeople.org and comes as part of the "Ruby Development Tools" feature for the Eclipse IDE framework.

测试NG

TestNG

Java xUnit 系列的成员之一,其行为方式与JUnit略有不同。TestNG特别支持测试之间的依赖关系以及测试方法之间测试装置的共享。更多信息请访问http://testng.org

A member of the xUnit family for Java that behaves a bit differently from JUnit. TestNG specifically supports dependencies between tests and the sharing of the test fixture between Test Methods. More information is available at http://testng.org.

PLSQL

utPLSQL

PLSQL 数据库编程语言 xUnit 系列的成员。您可以在http://utplsql.sourceforge.net/获得更多信息并下载此工具的源代码。在http://www.ounit.com上有一个将 utPLSQL 集成到 Oracle 工具集的插件。

The member of the xUnit family for the PLSQL database programming language. You can get more information and download the source for this tool at http://utplsql.sourceforge.net/. A plug-in that integrates utPLSQL into the Oracle toolset is available at http://www.ounit.com.

VB Lite 单元

VB Lite Unit

xUnit 家族的另一个成员,旨在支持 Visual Basic 和 VBA (Visual Basic for Applications)。“VB Lite Unit 是 Steve Jorgensen 编写的一款可靠的轻量级 Visual Basic 和 VBA 单元测试工具。VB Lite Unit 背后的驱动原则是创建最简单、最可靠的单元测试工具,该工具仍可完成在 VB 6 或 VBA 中进行测试驱动开发通常需要做的所有事情。避免在 VB 和 VBA 中不起作用或不可靠地工作,例如尝试进行自省以识别测试方法。”VB 和 VBA 程序员的另一个选择是 VbUnit。对于 VB.NET 程序员,选项包括 NUnit、CsUnit 和 MbUnit。

Another member of the xUnit family written to support Visual Basic and VBA (Visual Basic for Applications). "VB Lite Unit is a reliable, lightweight unit-testing tool for Visual Basic and VBA written by Steve Jorgensen. The driving principle behind VB Lite Unit was to create the simplest, most reliable unit-testing tool possible that would still do everything that usually matters for doing test-driven development in VB 6 or VBA. Things that don't work or don't work reliably in VB and VBA are avoided, such as attempts at introspection to identify the test methods." Another option for VB and VBA programmers is VbUnit. For VB.NET programmers, options include NUnit, CsUnit, and MbUnit.

单元

VbUnit

xUnit 家族的成员,为支持 Visual Basic 6.0 而编写。它是 xUnit 家族中第一个支持Suite Fixture Setup 的成员,并引入了将Testcase Class称为“测试装置”的概念。

The member of the xUnit family written to support Visual Basic 6.0. It was the first member of the xUnit family to support Suite Fixture Setup and introduced the concept of calling a Testcase Class "test fixture."

VbUnit的一个主要特点是,当断言方法测试失败时,它会立即将消息写入失败日志,而不是仅仅引发错误,然后由测试运行器捕获。这种行为的实际含义是,测试自定义断言变得困难,因为日志中的消息不会被正常的预期异常测试构造阻止。解决方法是在“封装的测试运行器”中运行自定义断言测试。

One major quirk of VbUnit is that when an Assertion Method fails the test, it writes the messages into the failure log immediately rather than just raising an error that is then caught by the Test Runner. The practical implication of this behavior is that it becomes difficult to test Custom Assertions because the messages in the logs are not prevented by the normal Expected Exception Test construct. The work-around is to run the Custom Assertion Tests inside an "Encapsulated Test Runner."

另一个奇怪的是,VbUnit是 xUnit 家族中少数几个不免费(如 beer)的成员之一。它可从http://www.vbunit.org获取。以前有一个免费版本可用 — 谁知道呢,也许有一天它会重新出现。VB 和 VBA 程序员的另一个选择是 VB Lite Unit。对于 VB.NET 程序员,选项包括 NUnit、CsUnit 和 MbUnit。

Another quirk is that VbUnit is one of the few members of the xUnit family that is not free (as in beer). It is available from http://www.vbunit.org. There used to be a free version available—who knows, it may reappear some day. Another option for VB and VBA programmers is VB Lite Unit. For VB.NET programmers, options include NUnit, CsUnit, and MbUnit.

单元

xUnit

任何以 JUnit 或 SUnit 为原型的单元测试自动化测试框架的通用名称。大多数语言的xUnit测试框架可在http://xprogramming.comhttp://en.wikipedia.org/wiki/XUnit找到。另一个可查找单元测试和客户测试工具的地方是http://www.opensourcetesting.org

The generic name for any Test Automation Framework for unit testing that is patterned on JUnit or SUnit. The xUnit test framework for most languages can be found at http://xprogramming.com or http://en.wikipedia.org/wiki/XUnit. Another place to look for both unit test and customer test tools is http://www.opensourcetesting.org.

附录 D

工具

Appendix D

Tools

 

本书中提到过以下工具。本节将更详细地介绍它们的用途以及它们与 xUnit 测试自动化的关系。

The following tools are mentioned at some point within this book. This section describes their purpose and how they relate to xUnit test automation in just a wee bit more detail.

蚂蚁

Ant

Java 社区使用的构建自动化工具。NAnt 相当于 .NET 项目。

A build automation tool used in the Java community. NAnt is the equivalent for .NET projects.

AntHill

Java社区使用的持续集成工具。

A continuous integration tool used in the Java community.

基础物理治疗

BPT

一个商业脚本测试工具,允许技术水平较低的用户使用可重用测试组件编写测试,这些组件是重构记录测试的结果。它还可用于指定可重用测试组件,供技术水平更高的测试自动化人员构建。更多信息可在 Mercury Interactive 的网站上找到。在本书付印时,Mercury Interactive 正在被惠普收购,因此 URL 可能已更改。

A commercial Scripted Test tool that allows less technically advanced users to compose tests from reusable test components that are the result of Refactored Recorded Tests. It can also be used to specify reusable test components to be built by more technically oriented test automaters. More information can be found on Mercury Interactive's Web site. As this book went to press, Mercury Interactive was in the process of being acquired by Hewlett-Packard, so the URL may have changed.

Canoo Web测试

Canoo WebTest

用于准备以 XML 编写的脚本测试的框架。从概念上讲, Canoo WebTest与 Fit 类似,因为它允许我们定义自己的领域特定测试语言来定义客户测试。更多信息请访问http://webtest.canoo.comhttp://webtest-community.canoo.com

A framework for preparing Scripted Tests written in XML. Conceptually, Canoo WebTest is similar to Fit in that it allows us to define our own domain-specific testing language for defining customer tests. More information can be found at http://webtest.canoo.com and http://webtest-community.canoo.com.

巡航控制

Cruise Control

Java 社区使用的持续集成工具。Cruise Control.net相当于 .NET 项目。

A continuous integration tool used in the Java community. Cruise Control.net is the equivalent for .NET projects.

DD步骤

DDSteps

JUnit 的数据驱动测试扩展。“ DDSteps是用于构建数据驱动测试用例的 JUnit 扩展。简而言之,DDSteps允许您参数化测试用例,并使用不同的数据多次运行它们。”有关更多信息,请参阅http://www.ddsteps.org 。

A Data-Driven Test extension for JUnit. "DDSteps is a JUnit extension for building data driven test cases. In a nutshell, DDSteps lets you parameterize your test cases, and run them more than once using different data." See http://www.ddsteps.org for more information.

易模拟

EasyMock

用于 Java 测试的静态模拟对象生成工具包。由于EasyMock使用配置模式来指定期望,因此测试看起来有点奇怪,可能需要一点时间来适应。更多信息可以在http://www.easymock.org上找到。

A static Mock Object generation toolkit for Java tests. Because EasyMock uses a Configuration Mode for specifying the expectations, the tests look a bit strange and may take a bit of getting used to. More information can be found at http://www.easymock.org.

计算机辅助测试

eCATT

SAP 开发工具附带的录制测试工具。更多信息请访问http://www.sap.comhttp://www.sdn.sap.com

The Recorded Test tool that comes with SAP's development tools. More information can be found at http://www.sap.com and at http://www.sdn.sap.com.

Eclipse

Java 集成开发环境 (IDE) 和富客户端应用程序平台。Eclipse最初由 IBM 创建,现在由Eclipse 基金会管理。一些特定于语言的插件与相应的 xUnit 系列成员集成。例如,Java IDE 包含 JUnit,而 Ruby Development Tools IDE 包含 Test::Unit。Eclipse可从http://www.eclipse.org下载。

A Java integrated development environment (IDE) and platform for rich client applications. Eclipse was originally created by IBM and is now managed by the Eclipse Foundation. Several of the language-specific plug-ins are integrated with the corresponding xUnit family member. For example, the Java IDE includes JUnit and the Ruby Development Tools IDE includes Test::Unit. Eclipse is available for download from http://www.eclipse.org.

合身

Fit

Ward Cunningham 构想的框架使客户能够编写自动化测试。Fit将使用网页或电子表格中的表格定义测试的工作与执行 SUT 的编程工作分开。虽然Fit曾经一个特定的工具,但现在它已成为用各种语言(包括 Java、.NET、Ruby 和 Python)实现的工具系列的规范。该系列的一些成员只是测试执行框架;其他成员(如 Fitnesse)包括测试创作和版本控制功能。所有成员都应实现同一套标准装置。更多信息可以在 Ward 的网站(http://fit.c2.com )或他与 Rick Mudgridge 合著的书籍[FitB]中找到。

The framework conceived by Ward Cunningham that made it possible for customers to write automated tests. Fit separates the work of defining the tests using tables in Web pages or spreadsheets from the programming work of exercising the SUT. While Fit was once a particular tool, it is now a specification for a family of tools implemented in a variety of languages, including Java, .NET, Ruby, and Python. Some members of the family are simply test execution frameworks; others, such as Fitnesse, include test authoring and versioning capabilities. All should implement the same set of standard fixtures. More information can be found at Ward's Web site (http://fit.c2.com) or in the book [FitB] he co-wrote with Rick Mudgridge.

健身

FitNesse

Object Mentor 的 Bob Martin (Uncle) 构想的 Fit 测试创作工具。FitNesse提供了一个类似 wiki 的测试创作系统,其中包含一组预定义的 Fit 装置,使客户能够编写和运行自动化测试。更多信息请访问http://www.fitnesse.org

A Fit test authoring tool conceived by (Uncle) Bob Martin of Object Mentor. FitNesse provides a wiki-like test authoring system with a set of predefined Fit fixtures that makes it possible for customers to write and run automated tests. More information can be found at http://www.fitnesse.org.

HttpUnit

HttpUnit

位于 JUnit 顶层的前端,允许测试通过 HTTP 协议测试 Web 应用程序。HttpUnit绕过浏览,因此不适合用于大量使用页面脚本(例如 AJAX)的应用程序。有关更多信息,请参阅http://httpunit.sourceforge.net 。

A front end that layers on top of JUnit to allow tests to exercise a Web application via the HTTP protocol. HttpUnit bypasses the browser, so it is not suitable for use with applications that make extensive use of on-page scripting (e.g., AJAX). See http://httpunit.sourceforge.net for more information.

主意

Idea

提供丰富重构支持的Java IDE。Idea网站[JBrains]包含许多重构的相当详细的描述。该团队还为 Visual Studio 提供了一个非常流行的重构插件,名为 ReSharper。

A Java IDE that offers rich support for refactoring. The Idea Web site [JBrains] contains fairly detailed descriptions of many of the refactorings. The same group also offers a very popular refactoring plug-in for Visual Studio, called ReSharper.

联合部队

JFCUnit

JUnit 前端位于 HttpUnit 之上,允许测试通过 HTTP 协议运行 Web 应用程序。JFCUnit提供了许多测试实用程序方法,这些方法构成了用于表达 Web 应用程序测试的高级语言。由于它是 HttpUnit 之上的一层,因此它绕过了浏览器。因此,JFCUnit不适合用于大量使用页面脚本(例如 AJAX)的应用程序。有关更多信息,请参阅http://jfcunit.sourceforge.net 。

A JUnit front end that layers on top of HttpUnit to allow tests to exercise a Web application via the HTTP protocol. JFCUnit provides a number of Test Utility Methods that form a Higher-Level Language for expressing tests of Web applications. Because it is a layer on top of HttpUnit, it bypasses the browser. Thus JFCUnit is not suitable for use with applications that make extensive use of on-page scripting (e.g., AJAX). See http://jfcunit.sourceforge.net for more information.

模拟

JMock

一种广泛使用的Java 测试动态模拟对象框架。用于指定期望的流畅配置接口使测试具有高度的可读性。更多信息可在http://www.jmock.org上找到。

A widely used dynamic Mock Object framework for Java tests. The fluent Configuration Interface used for specifying the expectations makes the tests highly readable. More information can be found at http://www.jmock.org.

NMock

NMock

广泛使用的动态Mock Object框架,用于 .NET 测试。用于指定期望的流畅配置接口使测试具有高度的可读性。更多信息可在http://nmock.org上找到。

A widely used dynamic Mock Object framework for .NET tests. The fluent Configuration Interface used for specifying the expectations makes the tests highly readable. More information can be found at http://nmock.org.

QTP(QuickTest 专业版)

QTP (QuickTest Professional)

一款商业记录测试工具,允许技术水平较低的用户在使用应用程序时记录测试。与记录测试的“专家视图”结合使用, QTP还可用于将测试重构为可重复使用的测试组件,适合技术水平较低的测试自动化人员使用。更多信息可在 Mercury Interactive 的网站上找到。本书付印时,Mercury Interactive 正被惠普收购,因此 URL 可能已更改。

A commercial Recorded Test tool that allows less technically advanced users to record tests as they use an application. In conjunction with the "Expert View" of the Recorded Tests, QTP can also be used to refactor the tests into reusable test components that are appropriate for use by less technically adept test automaters. More information can be found on Mercury Interactive's Web site. As this book went to press, Mercury Interactive was in the process of being acquired by Hewlett-Packard, so the URL has probably changed.

重新搜索

ReSharper

JetBrains(Idea IDE 的制造商)为 Visual Studio 开发的重构插件。他们的网站[JBrains]包含许多重构的相当详细的描述。

A refactoring plug-in for Visual Studio by JetBrains, the makers of the Idea IDE. Their Web site [JBrains] contains fairly detailed descriptions of many of the refactorings.

Visual Studio

Visual Studio

Microsoft 的集成开发环境,用于开发 .NET 应用软件。Visual Studio有多个版本(价格各异),其中一些版本包括 MSTest 和代码/测试重构支持。第三方插件也可用于重构(参见[JBrains])和 xUnit(参见 CsUnit、MbUnit 和 NUnit)。

Microsoft's integrated development environment intended for developing .NET applications software. Visual Studio comes in several versions (at various price points), some of which include MSTest and code/test refactoring support. Third-party plug-ins are also available for both refactoring (see [JBrains]) and xUnit (see CsUnit, MbUnit, and NUnit).

瓦提尔

Watir

“使用 Ruby 进行 Web 应用程序测试”。这组组件允许我们使用Ruby 编程语言编写的脚本测试来驱动 Internet Explorer。更多信息可在http://wtr.rubyforge.org/上找到。

"Web Application Testing in Ruby." This set of components allows us to drive Internet Explorer from Scripted Tests written in the Ruby programming language. More information can be found at http://wtr.rubyforge.org/.

附录 E

目标和原则

Appendix E

Goals and Principles

 

图像

图像

图像

附录 F

气味、别名和原因

Appendix F

Smells, Aliases, and Causes

 

图像

图像

图像

图像

图像

图像

图像

附录 G

模式、别名和变体

Appendix G

Patterns, Aliases, and Variations

 

图像

图像

图像

图像

图像

图像

图像

图像

图像

图像

图像

图像

图像

图像

图像

图像

图像

图像

图像

图像

图像

图像

图像

图像

图像

图像

图像

图像

图像

图像

图像

图像

图像

图像

词汇表

Glossary

 

本词汇表包含作者对本书中使用的术语的定义。

This glossary contains the author's definitions of the terms used throughout this book.

验收测试

acceptance test

也称为

Also known as

用户验收测试 (UAT)

User Acceptance Test (UAT)

软件客户计划运行的客户测试,以帮助客户决定是否接受软件系统。验收测试通常在所有自动客户测试通过后手动运行。它们会测试系统的所有层(从用户界面到数据库),并且应该包括与应用程序所依赖的其他系统的任何集成。

A customer test that the customer of the software plans to run to help the customer decide whether he or she will accept the software system. Acceptance tests are usually run manually after all automated customer tests have passed. They exercise all layers of the system—from the user interface back to the database—and should include any integration with other systems on which the application depends.

访问器

accessor

通过返回对象的实例变量的值或提供设置其值的方式来提供对对象的实例变量的访问的方法。

A method that provides access to an instance variable of an object either by returning its value or by providing a way to set its value.

ACID

现代数据库确保事务的四个特质:

The four qualities of transactions that modern databases ensure:

  • 原子性:交易要么全部完成,要么全部不完成。
  • Atomic: A transaction is all or nothing.
  • 一致:事务内的所有操作都看到相同的世界视图。
  • Consistent: All operations within a transaction see the same view of the world.
  • Independent: Transactions are independent of one another (no cross-transaction leakage of changes).
  • Independent: Transactions are independent of one another (no cross-transaction leakage of changes).
  • Durable: Once committed, the changes made within a transaction are permanent (they don't just vanish for no reason!).
  • Durable: Once committed, the changes made within a transaction are permanent (they don't just vanish for no reason!).

agile method

agile method

A method of executing projects (typically, but not always, restricted to software) that reduces the cost of change and allows customers of the software to have more control over how much they spend and what they get for their money. Agile methods include eXtreme Programming, SCRUM, Feature-Driven Development (FDD), and Dynamic Systems Development Method (DSDM), among many others. A core practice of most agile methods is the use of automated unit tests.

A method of executing projects (typically, but not always, restricted to software) that reduces the cost of change and allows customers of the software to have more control over how much they spend and what they get for their money. Agile methods include eXtreme Programming, SCRUM, Feature-Driven Development (FDD), and Dynamic Systems Development Method (DSDM), among many others. A core practice of most agile methods is the use of automated unit tests.

annotation

annotation

A way of indicating something about something. JUnit version 4.0 uses annotations to indicate which classes are Testcase Classes and which methods are Test Methods; NUnit uses .NET attributes for this purpose.

A way of indicating something about something. JUnit version 4.0 uses annotations to indicate which classes are Testcase Classes and which methods are Test Methods; NUnit uses .NET attributes for this purpose.

anonymous inner class

anonymous inner class

An inner class in Java that is defined without a unique name. Anonymous inner classes are often used when defining Hard-Coded Test Doubles.

An inner class in Java that is defined without a unique name. Anonymous inner classes are often used when defining Hard-Coded Test Doubles.

anti-pattern

anti-pattern

A pattern that shouldn't be used because it is known to produce less than optimal results. Code smells, or their underlying causes, are a kind of anti-pattern.

A pattern that shouldn't be used because it is known to produce less than optimal results. Code smells, or their underlying causes, are a kind of anti-pattern.

application programming interface (API)

application programming interface (API)

The means by which other software can invoke some piece of functionality. In object-oriented software, an API consists of the classes and their publicly accessible methods. In procedural software, it consists of the module or package name plus the publicly accessible procedures.

The means by which other software can invoke some piece of functionality. In object-oriented software, an API consists of the classes and their publicly accessible methods. In procedural software, it consists of the module or package name plus the publicly accessible procedures.

aspect-oriented programming

aspect-oriented programming

An advanced software modularization technique that allows improved separation of concerns by "weaving" cross-cutting concerns into code after the affected software has been built but before it is executed.

An advanced software modularization technique that allows improved separation of concerns by "weaving" cross-cutting concerns into code after the affected software has been built but before it is executed.

assertion

assertion

A statement that something should be true. In xUnit-style Test Automation Frameworks, an assertion takes the form of an Assertion Method that fails when the actual outcome passed to it does not match the expected outcome.

A statement that something should be true. In xUnit-style Test Automation Frameworks, an assertion takes the form of an Assertion Method that fails when the actual outcome passed to it does not match the expected outcome.

asynchronous test

asynchronous test

A test that runs in a separate thread of control from the system under test (SUT) and interacts with it using asynchronous (i.e., "real") messages. An asynchronous test must coordinate its steps with those of the SUT because this interaction is not managed automatically by the runtime system. An asynchronous test may have to include delays to give the SUT enough time to finish execution before inspecting the outcome. Contrast this with a synchronous test, which interacts with the SUT via simple method calls.

A test that runs in a separate thread of control from the system under test (SUT) and interacts with it using asynchronous (i.e., "real") messages. An asynchronous test must coordinate its steps with those of the SUT because this interaction is not managed automatically by the runtime system. An asynchronous test may have to include delays to give the SUT enough time to finish execution before inspecting the outcome. Contrast this with a synchronous test, which interacts with the SUT via simple method calls.

attribute

attribute

A characteristic of something. The members of the xUnit family for the .NET languages use class and method attributes to indicate which classes are Testcase Classes and which methods are Test Methods. The term attribute is also a synonym for "instance variable" in some circles.

A characteristic of something. The members of the xUnit family for the .NET languages use class and method attributes to indicate which classes are Testcase Classes and which methods are Test Methods. The term attribute is also a synonym for "instance variable" in some circles.

back door

back door

An alternative interface to a system under test (SUT) that test software can use to inject indirect inputs into the SUT. A database is a common example of a back door, but it could also be any component that can be either manipulated to return test-specific values or replaced by a Test Double. Contrast this with the front door: the application programming interface (API).

An alternative interface to a system under test (SUT) that test software can use to inject indirect inputs into the SUT. A database is a common example of a back door, but it could also be any component that can be either manipulated to return test-specific values or replaced by a Test Double. Contrast this with the front door: the application programming interface (API).

BDUF

BDUF

"Big design up front" is the classic "waterfall" approach to software design. In BDUF, all requirements must be understood early in the project, and the software is designed to support those requirements in a single design "phase." Contrast this with the emergent design favored by agile projects.

"Big design up front" is the classic "waterfall" approach to software design. In BDUF, all requirements must be understood early in the project, and the software is designed to support those requirements in a single design "phase." Contrast this with the emergent design favored by agile projects.

behavior-driven development

behavior-driven development

A variation on the test-driven development process wherein the focus of the tests is to clearly describe the expected behavior of the system under test (SUT). The emphasis is on Tests as Documentation rather than merely using tests for verification.

Behavior-driven development can be done using traditional members of the xUnit family. New "members" of the family, however, have been built specifically to emphasize the change in focus. They include changes in terminology (e.g., "test" becomes "spec"; "fixture" becomes "context") and more explicit framework support for clarity of the specification.

A variation on the test-driven development process wherein the focus of the tests is to clearly describe the expected behavior of the system under test (SUT). The emphasis is on Tests as Documentation rather than merely using tests for verification.

Behavior-driven development can be done using traditional members of the xUnit family. New "members" of the family, however, have been built specifically to emphasize the change in focus. They include changes in terminology (e.g., "test" becomes "spec"; "fixture" becomes "context") and more explicit framework support for clarity of the specification.

behavior smell

behavior smell

A test smell we encounter while compiling or running tests. We don't have to be particularly observant to notice behavior smells, as they will present themselves to us via compile errors or test failures. See also: code smell, project smell.

A test smell we encounter while compiling or running tests. We don't have to be particularly observant to notice behavior smells, as they will present themselves to us via compile errors or test failures. See also: code smell, project smell.

black box

black box

A piece of software that we treat as an opaque object whose internal workings cannot be seen. Tests written for the black box can verify only externally visible behavior and are independent of the implementation inside the system under test (SUT).

A piece of software that we treat as an opaque object whose internal workings cannot be seen. Tests written for the black box can verify only externally visible behavior and are independent of the implementation inside the system under test (SUT).

block

block

A block of code that can be run. Many programming languages (most notably, Smalltalk and Ruby) use blocks (also known as "block closures") as a way of passing a chunk of code to a method, which can then run the code in its own context. Java's anonymous inner classes are a way to achieve the same thing without direct support for blocks. C# uses delegates for the same purpose.

A block of code that can be run. Many programming languages (most notably, Smalltalk and Ruby) use blocks (also known as "block closures") as a way of passing a chunk of code to a method, which can then run the code in its own context. Java's anonymous inner classes are a way to achieve the same thing without direct support for blocks. C# uses delegates for the same purpose.

block closure

block closure

See block.

See block.

boundary value

boundary value

An input value for a system under test (SUT) that is immediately adjacent to the boundary between two equivalence classes. Tests using two adjacent boundary values help us verify that the behavior changes with exactly the right input and that we don't have "off by one" problems.

An input value for a system under test (SUT) that is immediately adjacent to the boundary between two equivalence classes. Tests using two adjacent boundary values help us verify that the behavior changes with exactly the right input and that we don't have "off by one" problems.

built-in self-test

built-in self-test

A means of organizing test code in which the tests live inside the same module or class as the production code and are run automatically when the system is initialized.

A means of organizing test code in which the tests live inside the same module or class as the production code and are run automatically when the system is initialized.

business logic

business logic

The core logic related to the domain model of a business system. Because business logic usually reflects the results of many independent business decisions, it often seems anything but logical!

The core logic related to the domain model of a business system. Because business logic usually reflects the results of many independent business decisions, it often seems anything but logical!

class attribute

class attribute

An attribute that is placed on a class in the source code to tell the compiler or runtime system that this class is "special." In some variants of xUnit, class attributes are used to indicate that a class is a Testcase Class.

An attribute that is placed on a class in the source code to tell the compiler or runtime system that this class is "special." In some variants of xUnit, class attributes are used to indicate that a class is a Testcase Class.

class method

class method

A method that is associated with a class rather than an object. Class methods can be invoked using a classname.methodname notation [e.g., Assert.assertEquals(message,  expected,  actual);] and do not require an instance of the class to be invoked. Class methods cannot access instance methods or instance variables of objects; that is, they do not have access to self or this. In Java, a class method is called a static method. Other languages may use different names or keywords.

A method that is associated with a class rather than an object. Class methods can be invoked using a classname.methodname notation [e.g., Assert.assertEquals(message,  expected,  actual);] and do not require an instance of the class to be invoked. Class methods cannot access instance methods or instance variables of objects; that is, they do not have access to self or this. In Java, a class method is called a static method. Other languages may use different names or keywords.

class variable

class variable

A variable that is associated with a class rather than an instance of the class and is typically used to access information that all instances need to share. In some languages, class variables can be accessed using the syntax classname.variablename (e.g., TestHelper.lineFeedCharacter;). That is, they do not need to be accessed via self or this. In Java, a class variable is called a static variable. Other languages may use different names or keywords.

A variable that is associated with a class rather than an instance of the class and is typically used to access information that all instances need to share. In some languages, class variables can be accessed using the syntax classname.variablename (e.g., TestHelper.lineFeedCharacter;). That is, they do not need to be accessed via self or this. In Java, a class variable is called a static variable. Other languages may use different names or keywords.

closure

closure

See block.

See block.

code smell

code smell

The "classic" bad smell, as first described by Martin Fowler in [Ref]. Test automaters must recognize code smells that arise as they maintain test code. Code smells typically affect maintenance cost of tests but may also be early warning signs of behavior smells to follow.

See also: test smell, behavior smell, project smell.

The "classic" bad smell, as first described by Martin Fowler in [Ref]. Test automaters must recognize code smells that arise as they maintain test code. Code smells typically affect maintenance cost of tests but may also be early warning signs of behavior smells to follow.

See also: test smell, behavior smell, project smell.

component

component

A larger part of the overall system that is often separately deployable. Component-based development involves decomposing the overall functionality into a series of individual components that can be built and deployed separately. This allows sharing of the components between applications that need the same functionality. Each component is a consequence of one or more design decisions, although its behavior may also be traced back to some aspect of the requirements.

Components can take many forms, depending on the technology being employed. The Windows platform uses dynamic linked libraries (DLLs) or assemblies as components. The Java platform uses Java Archives (JARs). A service-oriented architecture (SOA) uses Web Services as its large-grained components. The components may implement front-end logic (e.g., a "File Open Dialog") or back-end logic (e.g., a "Customer Persistence" component). A component can and should be verified using component tests before the overall application is tested using customer tests.

A larger part of the overall system that is often separately deployable. Component-based development involves decomposing the overall functionality into a series of individual components that can be built and deployed separately. This allows sharing of the components between applications that need the same functionality. Each component is a consequence of one or more design decisions, although its behavior may also be traced back to some aspect of the requirements.

Components can take many forms, depending on the technology being employed. The Windows platform uses dynamic linked libraries (DLLs) or assemblies as components. The Java platform uses Java Archives (JARs). A service-oriented architecture (SOA) uses Web Services as its large-grained components. The components may implement front-end logic (e.g., a "File Open Dialog") or back-end logic (e.g., a "Customer Persistence" component). A component can and should be verified using component tests before the overall application is tested using customer tests.

component test

component test

A test that verifies the behavior of some component of the overall system. The component is a consequence of one or more design decisions, although its behavior may also be traced back to some aspect of the requirements. There is no need for component tests to be readable, recognizable, or verifiable by the customer or business domain expert. Contrast this with a customer test, which is derived almost entirely from the requirements and should be verifiable by the customer, and with a unit test, which verifies a much smaller component. A component test lies somewhere in between these two extremes.

During test-driven development, component tests are written after the customer tests are written and the overall design is solidified. They are written as the architectural decisions are made but before the individual units are designed or coded. They are usually automated using a member of the xUnit family.

A test that verifies the behavior of some component of the overall system. The component is a consequence of one or more design decisions, although its behavior may also be traced back to some aspect of the requirements. There is no need for component tests to be readable, recognizable, or verifiable by the customer or business domain expert. Contrast this with a customer test, which is derived almost entirely from the requirements and should be verifiable by the customer, and with a unit test, which verifies a much smaller component. A component test lies somewhere in between these two extremes.

During test-driven development, component tests are written after the customer tests are written and the overall design is solidified. They are written as the architectural decisions are made but before the individual units are designed or coded. They are usually automated using a member of the xUnit family.

constructor

constructor

A special method used in some object-oriented programming languages to construct a brand-new object. It often has the same name as the class and is typically called automatically by the runtime system whenever the special operation new is invoked. A Complete Constructor Method [SBPP] returns a ready-to-use object that requires no additional tweaking; this usually implies arguments must be passed to the constructor.

A special method used in some object-oriented programming languages to construct a brand-new object. It often has the same name as the class and is typically called automatically by the runtime system whenever the special operation new is invoked. A Complete Constructor Method [SBPP] returns a ready-to-use object that requires no additional tweaking; this usually implies arguments must be passed to the constructor.

continuous integration

continuous integration

The agile software development practice of integrating software changes continuously. In practice, developers typically integrate their changes every few hours to days. Continuous integration often includes the practice of an automated build that is triggered by each check-in. The build process typically runs all automated tests and may even run tests that aren't run before check-in because they take too long. The build is considered to have "failed" if any tests fail. When the build fails, teams typically consider getting the build working again to be the top priority; only code changes aimed at fixing the build are allowed until a successful build has occurred.

The agile software development practice of integrating software changes continuously. In practice, developers typically integrate their changes every few hours to days. Continuous integration often includes the practice of an automated build that is triggered by each check-in. The build process typically runs all automated tests and may even run tests that aren't run before check-in because they take too long. The build is considered to have "failed" if any tests fail. When the build fails, teams typically consider getting the build working again to be the top priority; only code changes aimed at fixing the build are allowed until a successful build has occurred.

control point

control point

How the test asks the system under test (SUT) to do something for it. A control point could be created for the purpose of setting up or tearing down the fixture or it could be used during the exercise SUT phase of the test. It is a kind of interaction point. Some control points are provided strictly for testing purposes; they should not be used by the production code because they bypass input validation or short-circuit the normal life cycle of the SUT or some object on which it depends.

How the test asks the system under test (SUT) to do something for it. A control point could be created for the purpose of setting up or tearing down the fixture or it could be used during the exercise SUT phase of the test. It is a kind of interaction point. Some control points are provided strictly for testing purposes; they should not be used by the production code because they bypass input validation or short-circuit the normal life cycle of the SUT or some object on which it depends.

customer test

customer test

A test that verifies the behavior of a slice of the visible functionality of the overall system. The system under test (SUT) may consist of the entire system or a fully functional top-to-bottom slice ("module") of the system. A customer test should be independent of the design decisions made while building the SUT. That is, we should require the same set of customer tests regardless of how we choose to build the SUT. (Of course, how the customer tests interact with the SUT may be affected by high-level software architecture decisions.)

A test that verifies the behavior of a slice of the visible functionality of the overall system. The system under test (SUT) may consist of the entire system or a fully functional top-to-bottom slice ("module") of the system. A customer test should be independent of the design decisions made while building the SUT. That is, we should require the same set of customer tests regardless of how we choose to build the SUT. (Of course, how the customer tests interact with the SUT may be affected by high-level software architecture decisions.)

data access layer

data access layer

A way of keeping data access logic from permeating the application code by putting it into a separate component that encapsulates the database.

A way of keeping data access logic from permeating the application code by putting it into a separate component that encapsulates the database.

Also known as

Also known as

data access object (DAO), data abstraction layer (DAL)

data access object (DAO), data abstraction layer (DAL)

depended-on component (DOC)

depended-on component (DOC)

An individual class or a large-grained component on which the system under test (SUT) depends. The dependency is usually one of delegation via method calls. In test automation, the DOC is primarily of interest in that we need to be able to observe and control its interactions with the SUT to get complete test coverage.

An individual class or a large-grained component on which the system under test (SUT) depends. The dependency is usually one of delegation via method calls. In test automation, the DOC is primarily of interest in that we need to be able to observe and control its interactions with the SUT to get complete test coverage.

design pattern

design pattern

A pattern that we can use to solve a particular software design problem. Most design patterns are programming language independent; the language-specific ones are typically called "coding idioms." Design patterns were first popularized by the book Design Patterns [GOF].

A pattern that we can use to solve a particular software design problem. Most design patterns are programming language independent; the language-specific ones are typically called "coding idioms." Design patterns were first popularized by the book Design Patterns [GOF].

design for testability

design for testability

A way of ensuring that code is easily tested by making sure that testing requirements are considered as the code is designed. When doing test-driven development, design for testability occurs as a natural side effect of development

A way of ensuring that code is easily tested by making sure that testing requirements are considered as the code is designed. When doing test-driven development, design for testability occurs as a natural side effect of development

Also known as

Also known as

DfT

DfT

developer test

developer test

Another name for an automated unit test that is prepared by someone playing the developer role on an eXtreme Programming project.

Another name for an automated unit test that is prepared by someone playing the developer role on an eXtreme Programming project.

DfT

DfT

See design for testability.

See design for testability.

direct input

direct input

A test may interact with the system under test (SUT) directly via its "front door" or public application programming interface (API) or indirectly via its "back door." The stimuli injected by the test into the SUT via its front door are direct inputs of the SUT. Direct inputs may consist of method or function calls to another component or messages sent on a message channel (e.g., MQ or JMS) and the arguments or contents thereof.

A test may interact with the system under test (SUT) directly via its "front door" or public application programming interface (API) or indirectly via its "back door." The stimuli injected by the test into the SUT via its front door are direct inputs of the SUT. Direct inputs may consist of method or function calls to another component or messages sent on a message channel (e.g., MQ or JMS) and the arguments or contents thereof.

direct output

direct output

A test may interact with the system under test (SUT) directly via its "front door" or public application programming interface (API) or indirectly via its "back door." The responses received by the test from the SUT via its front door are direct outputs of the SUT. Direct outputs may consist of the return values of method or function calls, updated arguments passed by reference, exceptions raised by the SUT, or messages received on a message channel (e.g., MQ or JMS) from the SUT.

A test may interact with the system under test (SUT) directly via its "front door" or public application programming interface (API) or indirectly via its "back door." The responses received by the test from the SUT via its front door are direct outputs of the SUT. Direct outputs may consist of the return values of method or function calls, updated arguments passed by reference, exceptions raised by the SUT, or messages received on a message channel (e.g., MQ or JMS) from the SUT.

document-driven development

document-driven development

A development process that focuses on producing documents that describe how the code will be structured and then coding from those documents. Document-driven development is normally associated with "big design up front" (BDUF, also known as "waterfall") software development. Contrast this with test-driven development, which focuses on producing working code one test at a time.

A development process that focuses on producing documents that describe how the code will be structured and then coding from those documents. Document-driven development is normally associated with "big design up front" (BDUF, also known as "waterfall") software development. Contrast this with test-driven development, which focuses on producing working code one test at a time.

domain layer

domain layer

The layer of a Layered Architecture [DDD, PEAA, WWW] that corresponds to the domain model. See Eric Evans' book, Domain-Driven Design [DDD].

The layer of a Layered Architecture [DDD, PEAA, WWW] that corresponds to the domain model. See Eric Evans' book, Domain-Driven Design [DDD].

domain model

domain model

A model of the problem domain that may form the basis of the object model in the business domain layer of a software application. See Eric Evans' book, Domain-Driven Design [DDD].

A model of the problem domain that may form the basis of the object model in the business domain layer of a software application. See Eric Evans' book, Domain-Driven Design [DDD].

DTO

DTO

Short for the Data Transfer Object [CJ2EEP] design pattern.

Short for the Data Transfer Object [CJ2EEP] design pattern.

dynamic binding

dynamic binding

Deferring the decision about which piece of software to transfer control to until execution time. The same method name can be used to invoke different behavior (method bodies) based on the class of the object on which it is invoked; the latter class is determined only at execution time. Dynamic binding is the opposite of static binding; it is also called polymorphism (from the Latin, meaning "taking on many shapes").

Deferring the decision about which piece of software to transfer control to until execution time. The same method name can be used to invoke different behavior (method bodies) based on the class of the object on which it is invoked; the latter class is determined only at execution time. Dynamic binding is the opposite of static binding; it is also called polymorphism (from the Latin, meaning "taking on many shapes").

EDD

EDD

See example-driven development.

See example-driven development.

emergent design

emergent design

The opposite of BDUF (big design up front). Emergent design involves letting the right design be discovered as the software slowly evolves to pass one test at a time during test-driven development.

The opposite of BDUF (big design up front). Emergent design involves letting the right design be discovered as the software slowly evolves to pass one test at a time during test-driven development.

endoscopic testing

endoscopic testing

A testing technique pioneered by the authors of the original Mock Object paper [ET], which involves testing software from the inside.

A testing technique pioneered by the authors of the original Mock Object paper [ET], which involves testing software from the inside.

entity object

entity object

An object that represents an entity concept from a domain. Entity objects typically have a life cycle that is represented as their state. Contrast this with a service object, which has no single state. EJB Entity Beans are one example of an entity object.

An object that represents an entity concept from a domain. Entity objects typically have a life cycle that is represented as their state. Contrast this with a service object, which has no single state. EJB Entity Beans are one example of an entity object.

Also known as

Also known as

domain object

domain object

equivalence class

equivalence class

A test condition identification technique that reduces the number of tests required by grouping together inputs that should result in the same output or that should exercise the same logic in the system. This organization allows us to focus our tests on key boundary values at which the expected output changes.

A test condition identification technique that reduces the number of tests required by grouping together inputs that should result in the same output or that should exercise the same logic in the system. This organization allows us to focus our tests on key boundary values at which the expected output changes.

example-driven development (EDD)

example-driven development (EDD)

A reframing of the test-driven development process to focus on the "executable specification" aspect of the tests. The act of providing examples is more intuitive to many people; it doesn't carry the baggage of "testing" software that doesn't yet exist.

A reframing of the test-driven development process to focus on the "executable specification" aspect of the tests. The act of providing examples is more intuitive to many people; it doesn't carry the baggage of "testing" software that doesn't yet exist.

exercise SUT

exercise SUT

After the fixture setup phase of testing, the test stimulates the system under test (SUT) logic that is to be tested. This phase of the testing process is called exercise SUT.

After the fixture setup phase of testing, the test stimulates the system under test (SUT) logic that is to be tested. This phase of the testing process is called exercise SUT.

expectation

expectation

What a test expects the system under test (SUT) to have done. When we are using Mock Objects to verify the indirect outputs of the SUT, we load each Mock Object with the expected method calls (including the expected arguments); these are called the expectations.

What a test expects the system under test (SUT) to have done. When we are using Mock Objects to verify the indirect outputs of the SUT, we load each Mock Object with the expected method calls (including the expected arguments); these are called the expectations.

expected outcome

expected outcome

The outcome that we verify after exercising the system under test (SUT). A Self-Checking Test verifies the expected outcome using calls to Assertion Methods.

The outcome that we verify after exercising the system under test (SUT). A Self-Checking Test verifies the expected outcome using calls to Assertion Methods.

exploratory testing

exploratory testing

Interactive testing of an application without a specific script in hand. The tester "explores" the system, making up theories about how it should behave based on what the application has already done and then testing those theories to see if they hold up. While there is no rigid plan, exploratory testing is a disciplined activity that is more likely to find real bugs than rigidly scripted tests.

Interactive testing of an application without a specific script in hand. The tester "explores" the system, making up theories about how it should behave based on what the application has already done and then testing those theories to see if they hold up. While there is no rigid plan, exploratory testing is a disciplined activity that is more likely to find real bugs than rigidly scripted tests.

eXtreme Programming

eXtreme Programming

An agile software development methodology that showcases pair programming, automated unit testing, and short iterations.

An agile software development methodology that showcases pair programming, automated unit testing, and short iterations.

factory

factory

A method, object, or class that exists to build other objects.

A method, object, or class that exists to build other objects.

false negative

false negative

A situation in which a test passes even though the system under test (SUT) is not working properly. Such a test is said to give a false-negative indication or a "false pass."

See also: false positive.

A situation in which a test passes even though the system under test (SUT) is not working properly. Such a test is said to give a false-negative indication or a "false pass."

See also: false positive.

false positive

false positive

A situation in which a test fails even though the system under test (SUT) is working properly. Such a test is said to give a false-positive indication or a "false failure." The terminology comes from statistical science and relates to our attempt to calculate the probability of some observation error occurring. For example, in medicine we run tests to find out if a medical condition is present; if it is, the test is "positive." It is useful to know the probability that a test might indicate that a condition (such as diabetes) is present when it is not—that is, a false "positive." If we think of software tests as a way of determining whether a condition (a particular defect or bug) is present, a test that reports a defect (a test failure or error) when it is not, in fact, present is giving us a false positive.

See also: false negative. Wikipedia [Wp] has an extensive description under the topic "Type I and type II errors."

A situation in which a test fails even though the system under test (SUT) is working properly. Such a test is said to give a false-positive indication or a "false failure." The terminology comes from statistical science and relates to our attempt to calculate the probability of some observation error occurring. For example, in medicine we run tests to find out if a medical condition is present; if it is, the test is "positive." It is useful to know the probability that a test might indicate that a condition (such as diabetes) is present when it is not—that is, a false "positive." If we think of software tests as a way of determining whether a condition (a particular defect or bug) is present, a test that reports a defect (a test failure or error) when it is not, in fact, present is giving us a false positive.

See also: false negative. Wikipedia [Wp] has an extensive description under the topic "Type I and type II errors."

fault insertion test

fault insertion test

A kind of test in which a deliberate fault is introduced in one part of the system to verify that another part reacts to the error appropriately. Initially, the faults were related to hardware but the same concept is now applied to software faults as well. Replacing a depended-on component (DOC) with a Saboteur that throws an exception is an example of a software fault insertion test.

A kind of test in which a deliberate fault is introduced in one part of the system to verify that another part reacts to the error appropriately. Initially, the faults were related to hardware but the same concept is now applied to software faults as well. Replacing a depended-on component (DOC) with a Saboteur that throws an exception is an example of a software fault insertion test.

feature

feature

A testable unit of functionality that can be built onto the evolving software system. In eXtreme Programming, a user story corresponds roughly to a feature.

A testable unit of functionality that can be built onto the evolving software system. In eXtreme Programming, a user story corresponds roughly to a feature.

Fit test

Fit test

A test that uses the Fit testing framework; most commonly a customer test.

A test that uses the Fit testing framework; most commonly a customer test.

fixture

fixture

See test fixture (disambiguation).

See test fixture (disambiguation).

fixture (Fit)

fixture (Fit)

In Fit, the Adapter [GOF] that interprets the Fit table and invokes methods on the system under test (SUT), thereby implementing a Data-Driven Test. For meanings in other contexts, see test fixture (disambiguation), test fixture (in xUnit), and test fixture (in NUnit).

In Fit, the Adapter [GOF] that interprets the Fit table and invokes methods on the system under test (SUT), thereby implementing a Data-Driven Test. For meanings in other contexts, see test fixture (disambiguation), test fixture (in xUnit), and test fixture (in NUnit).

fixture holding class variable

fixture holding class variable

A class variable of a Testcase Class that is used to hold a reference to the test fixture. It typically holds a reference to a Shared Fixture.

A class variable of a Testcase Class that is used to hold a reference to the test fixture. It typically holds a reference to a Shared Fixture.

fixture holding instance variable

fixture holding instance variable

An instance variable of a Testcase Object that is used to hold a reference to the test fixture. It typically holds a reference to a Fresh Fixture that is set up using Implicit Setup.

An instance variable of a Testcase Object that is used to hold a reference to the test fixture. It typically holds a reference to a Fresh Fixture that is set up using Implicit Setup.

fixture holding local variable

fixture holding local variable

A local variable of a Test Method that is used to hold a reference to the test fixture. It typically holds a reference to a Fresh Fixture that is set up within the test method using In-line Setup or returned from Delegated Setup.

A local variable of a Test Method that is used to hold a reference to the test fixture. It typically holds a reference to a Fresh Fixture that is set up within the test method using In-line Setup or returned from Delegated Setup.

fixture setup

fixture setup

Before the desired logic of the system under test (SUT) can be exercised, the pre-conditions of the test need to be set up. Collectively, all objects (and their states) are called the test fixture (or test context), and the phase of the test that sets up the test fixture is called fixture setup.

Before the desired logic of the system under test (SUT) can be exercised, the pre-conditions of the test need to be set up. Collectively, all objects (and their states) are called the test fixture (or test context), and the phase of the test that sets up the test fixture is called fixture setup.

fixture teardown

fixture teardown

After a test is run, the test fixture that was built by the test should be destroyed. This phase of the test is called fixture teardown.

After a test is run, the test fixture that was built by the test should be destroyed. This phase of the test is called fixture teardown.

fluent interface

fluent interface

A style of object constructor API that results in easy-to-understand statements. The Configuration Interface provided by the Mock Object toolkit JMock is an example of a fluent interface.

A style of object constructor API that results in easy-to-understand statements. The Configuration Interface provided by the Mock Object toolkit JMock is an example of a fluent interface.

front door

front door

The public application programming interface (API) of a piece of software. Contrast this with the back door.

The public application programming interface (API) of a piece of software. Contrast this with the back door.

function pointer

function pointer

From Wikipedia [Wp]: "A function pointer is a type of pointer in C, C++, D, and other C-like programming languages. When dereferenced, a function pointer invokes a function, passing it zero or more arguments like a normal function."

From Wikipedia [Wp]: "A function pointer is a type of pointer in C, C++, D, and other C-like programming languages. When dereferenced, a function pointer invokes a function, passing it zero or more arguments like a normal function."

Also known as

Also known as

procedure variable, delegate in .NET languages

procedure variable, delegate in .NET languages

functional test (common usage)

functional test (common usage)

A black-box test of the end-user functionality of an application. The agile community is trying to avoid this usage of "functional test" because of the potential for confusion when talking about verifying functional (as opposed to nonfunctional or extra-functional properties) properties of a unit or component. This book uses the terms "customer test" and "acceptance test" for a functional test of the entire application and "unit test" for a functional test of an individual unit of the application.

A black-box test of the end-user functionality of an application. The agile community is trying to avoid this usage of "functional test" because of the potential for confusion when talking about verifying functional (as opposed to nonfunctional or extra-functional properties) properties of a unit or component. This book uses the terms "customer test" and "acceptance test" for a functional test of the entire application and "unit test" for a functional test of an individual unit of the application.

functional test (contrast with extra-functional test)

functional test (contrast with extra-functional test)

A test that verifies the functionality implemented by a piece of software. Depending on the scope of the software, a functional test may be a customer test, a unit test, or a component test.

In some circles a functional test is a customer test. This usage becomes confusing, however, when we talk about testing nonfunctional or extra-functional properties of the system under test (SUT). This book uses the terms "customer test" and "acceptance test" for a functional test of the entire application and "unit test" for a functional test of an individual unit of the application.

A test that verifies the functionality implemented by a piece of software. Depending on the scope of the software, a functional test may be a customer test, a unit test, or a component test.

In some circles a functional test is a customer test. This usage becomes confusing, however, when we talk about testing nonfunctional or extra-functional properties of the system under test (SUT). This book uses the terms "customer test" and "acceptance test" for a functional test of the entire application and "unit test" for a functional test of an individual unit of the application.

garbage collection

garbage collection

A mechanism that automatically recovers the memory used by any objects that are no longer accessible. Many modern object-oriented programming environments provide garbage collection.

A mechanism that automatically recovers the memory used by any objects that are no longer accessible. Many modern object-oriented programming environments provide garbage collection.

global variable

global variable

A variable that is global to a whole program. A global variable is accessible from anywhere within the program and never goes out of scope, although the memory to which it refers can be deallocated explicitly.

A variable that is global to a whole program. A global variable is accessible from anywhere within the program and never goes out of scope, although the memory to which it refers can be deallocated explicitly.

green bar

green bar

Many Graphical Test Runners portray the progress of the test run using a progress bar. As long as all tests have passed, the bar stays green. When any tests fail, the indicator changes to a red bar.

Many Graphical Test Runners portray the progress of the test run using a progress bar. As long as all tests have passed, the bar stays green. When any tests fail, the indicator changes to a red bar.

GUI

GUI

Graphical user interface.

Graphical user interface.

happy path

happy path

The "normal" path of execution through a use case or through the software that implements it; also known as the "sunny day" scenario. Nothing goes wrong, nothing out of the ordinary happens, and we swiftly and directly achieve the user's or caller's goal.

The "normal" path of execution through a use case or through the software that implements it; also known as the "sunny day" scenario. Nothing goes wrong, nothing out of the ordinary happens, and we swiftly and directly achieve the user's or caller's goal.

Hollywood principle

Hollywood principle

What directors in Hollywood tell aspiring actors at mass-casting calls: "Don't call us; we'll call you (if we want you)." In software, this concept is often called inversion of control (IOC).

What directors in Hollywood tell aspiring actors at mass-casting calls: "Don't call us; we'll call you (if we want you)." In software, this concept is often called inversion of control (IOC).

IDE

IDE

Integrated development environment. An environment that provides tools to edit, compile, execute, and (typically) test code within a single development tool.

Integrated development environment. An environment that provides tools to edit, compile, execute, and (typically) test code within a single development tool.

incremental delivery

incremental delivery

A method of building and deploying a software system in stages and releasing the software as each stage, called an "increment," is completed. This approach results in earlier delivery to the user of a working system, where the capabilities of the system increase over time. In agile methods, the increment of functionality is the feature or user story. Incremental delivery goes beyond iterative development and incremental development, however, by actually putting the functionality into production on a regular basis. This idea is summarized by the following mantra: "Deliver early, deliver often."

A method of building and deploying a software system in stages and releasing the software as each stage, called an "increment," is completed. This approach results in earlier delivery to the user of a working system, where the capabilities of the system increase over time. In agile methods, the increment of functionality is the feature or user story. Incremental delivery goes beyond iterative development and incremental development, however, by actually putting the functionality into production on a regular basis. This idea is summarized by the following mantra: "Deliver early, deliver often."

incremental development

incremental development

A method of building a software system in stages such that the functionality built to date can be tested before the next stage is started. This approach allows for the earlier delivery to the user of a working system, where the capabilities of the system increase over time (see incremental delivery). In agile methods, the increment of functionality is the feature or user story. Incremental development goes beyond iterative development, however, in that it promises to produce working, testable, and potentially deployable software with every iteration. With incremental delivery, we also promise to "Deliver early, deliver often."

A method of building a software system in stages such that the functionality built to date can be tested before the next stage is started. This approach allows for the earlier delivery to the user of a working system, where the capabilities of the system increase over time (see incremental delivery). In agile methods, the increment of functionality is the feature or user story. Incremental development goes beyond iterative development, however, in that it promises to produce working, testable, and potentially deployable software with every iteration. With incremental delivery, we also promise to "Deliver early, deliver often."

indirect input

indirect input

When the behavior of the system under test (SUT) is affected by the values returned by another component whose services it uses, we call those values the indirect inputs of the SUT. Indirect inputs may consist of actual return values of functions, updated (out) parameters of procedures or subroutines, and any errors or exceptions raised by the depended-on component (DOC). Testing of the SUT behavior with indirect inputs requires the appropriate control point on the "back side" of the SUT. We often use a Test Stub to inject the indirect inputs into the SUT.

When the behavior of the system under test (SUT) is affected by the values returned by another component whose services it uses, we call those values the indirect inputs of the SUT. Indirect inputs may consist of actual return values of functions, updated (out) parameters of procedures or subroutines, and any errors or exceptions raised by the depended-on component (DOC). Testing of the SUT behavior with indirect inputs requires the appropriate control point on the "back side" of the SUT. We often use a Test Stub to inject the indirect inputs into the SUT.

indirect output

indirect output

When the behavior of the system under test (SUT) includes actions that cannot be observed through the public application programming interface (API) of the SUT but that are seen or experienced by other systems or application components, we call those actions the indirect outputs of the SUT. Indirect outputs may consist of method or function calls to another component, messages sent on a message channel (e.g., MQ or JMS), and records inserted into a database or written to a file. Verification of the indirect output behaviors of the SUT requires the use of appropriate observation points on the "back side" of the SUT. Mock Objects are often used to implement the observation point by intercepting the indirect outputs of the SUT and comparing them to the expected values.

See also: outgoing interface.

When the behavior of the system under test (SUT) includes actions that cannot be observed through the public application programming interface (API) of the SUT but that are seen or experienced by other systems or application components, we call those actions the indirect outputs of the SUT. Indirect outputs may consist of method or function calls to another component, messages sent on a message channel (e.g., MQ or JMS), and records inserted into a database or written to a file. Verification of the indirect output behaviors of the SUT requires the use of appropriate observation points on the "back side" of the SUT. Mock Objects are often used to implement the observation point by intercepting the indirect outputs of the SUT and comparing them to the expected values.

See also: outgoing interface.

Also known as

Also known as

outgoing interface

outgoing interface

inner class

inner class

A class in Java that is defined inside another class. Anonymous inner classes are defined inside a method, whereas inner classes are defined outside a method. Inner classes are often used when defining Hard-Coded Test Doubles.

A class in Java that is defined inside another class. Anonymous inner classes are defined inside a method, whereas inner classes are defined outside a method. Inner classes are often used when defining Hard-Coded Test Doubles.

Also known as

Also known as

member variable

member variable

instance method

instance method

A method that is associated with an object rather than the class of the object. An instance method is accessible only from within or via an instance of the class. It is typically used to access information that is expected to differ from one instance to another.

The exact syntax used to access an instance method varies from language to language. The most common syntax is objectReference.methodName(). When referenced from within other methods on the object, some languages require an explicit reference to the object (e.g., this.methodName() or self methodName); other languages simply assume that any unqualified references to methods are references to instance methods.

A method that is associated with an object rather than the class of the object. An instance method is accessible only from within or via an instance of the class. It is typically used to access information that is expected to differ from one instance to another.

The exact syntax used to access an instance method varies from language to language. The most common syntax is objectReference.methodName(). When referenced from within other methods on the object, some languages require an explicit reference to the object (e.g., this.methodName() or self methodName); other languages simply assume that any unqualified references to methods are references to instance methods.

instance variable

instance variable

A variable that is associated with an object rather than the class of object. An instance variable is accessible only from within or via an instance of the class. It is typically used to access information that is expected to differ from one instance to another.

A variable that is associated with an object rather than the class of object. An instance variable is accessible only from within or via an instance of the class. It is typically used to access information that is expected to differ from one instance to another.

Also known as

Also known as

member function

member function

interaction point

interaction point

A point at which a test interacts with the system under test (SUT). An interaction point can be either a control point or an observation point.

A point at which a test interacts with the system under test (SUT). An interaction point can be either a control point or an observation point.

interface

interface

In general, a fully abstract class that defines only the public methods that all implementers of the interface must provide. In Java, an interface is a type definition that does not provide any implementation. In most single-inheritance languages, a class may implement any number of interfaces, even though it can extend (subclass) only one other class.

In general, a fully abstract class that defines only the public methods that all implementers of the interface must provide. In Java, an interface is a type definition that does not provide any implementation. In most single-inheritance languages, a class may implement any number of interfaces, even though it can extend (subclass) only one other class.

inversion of control (IOC)

inversion of control (IOC)

A control paradigm that distinguishes software frameworks from "toolkits" or components. The framework calls the software plug-in (rather than the reverse). In the real world, inversion of control is often called the Hollywood principle. With the advent of automated unit testing, a class of framework known as an inversion of control framework has sprung up specifically to simplify the replacement of depended-on components (DOCs) with Test Doubles.

A control paradigm that distinguishes software frameworks from "toolkits" or components. The framework calls the software plug-in (rather than the reverse). In the real world, inversion of control is often called the Hollywood principle. With the advent of automated unit testing, a class of framework known as an inversion of control framework has sprung up specifically to simplify the replacement of depended-on components (DOCs) with Test Doubles.

IOC

IOC

See inversion of control.

See inversion of control.

iterative development

iterative development

A method of building a software system using time-boxed "iterations." Each iteration is planned and then executed. At the end of the "time box," the status of all the work is reviewed and the next iteration is planned. The strict time-boxing prevents "runaway development," where the state of the system is never assessed because nothing is ever finished. Unlike incremental development, iterative development does not require working software to be delivered at the end of each iteration.

A method of building a software system using time-boxed "iterations." Each iteration is planned and then executed. At the end of the "time box," the status of all the work is reviewed and the next iteration is planned. The strict time-boxing prevents "runaway development," where the state of the system is never assessed because nothing is ever finished. Unlike incremental development, iterative development does not require working software to be delivered at the end of each iteration.

layer-crossing test

layer-crossing test

A test that either sets up the fixture or verifies the outcome using Back Door Manipulation, which involves using a "back door" of the system under test (SUT) such as a database. Contrast this with a round-trip test.

A test that either sets up the fixture or verifies the outcome using Back Door Manipulation, which involves using a "back door" of the system under test (SUT) such as a database. Contrast this with a round-trip test.

legacy software

legacy software

In the test-driven development community, any software that does not have a Safety Net of Fully Automated Tests.

In the test-driven development community, any software that does not have a Safety Net of Fully Automated Tests.

liveware

liveware

The people who use our software. They are usually assumed to be much more intelligent than either the software or the hardware but they can also be rather unpredictable.

The people who use our software. They are usually assumed to be much more intelligent than either the software or the hardware but they can also be rather unpredictable.

Also known as

Also known as

wetware, mushware

wetware, mushware

local variable

local variable

A variable that is associated with a block of code rather than an object or class. A local variable is accessible only from within the code block; it goes out of scope when the block of code returns to its caller.

A variable that is associated with a block of code rather than an object or class. A local variable is accessible only from within the code block; it goes out of scope when the block of code returns to its caller.

manual test

manual test

A test that is executed by a person interacting with the system under test (SUT). The user may be following some sort of "test script" (not to be confused with a Scripted Test) or doing ad hoc or exploratory testing.

A test that is executed by a person interacting with the system under test (SUT). The user may be following some sort of "test script" (not to be confused with a Scripted Test) or doing ad hoc or exploratory testing.

meta object

meta object

An object that holds data that controls the behavior of another object. A meta object protocol is the interface by which the meta object is constructed or configured.

An object that holds data that controls the behavior of another object. A meta object protocol is the interface by which the meta object is constructed or configured.

metatest

metatest

A test that verifies the behavior of one or more tests. Such a test is mostly used during test-driven development, when we are writing tests as examples or course material and we want to ensure that tests are, indeed, failing to illustrate a particular problem.

A test that verifies the behavior of one or more tests. Such a test is mostly used during test-driven development, when we are writing tests as examples or course material and we want to ensure that tests are, indeed, failing to illustrate a particular problem.

method attribute

method attribute

An attribute that is placed on a method in the source code to tell the compiler or runtime system that this method is "special." In some xUnit family members, method attributes are used to indicate that a method is a Test Method.

An attribute that is placed on a method in the source code to tell the compiler or runtime system that this method is "special." In some xUnit family members, method attributes are used to indicate that a method is a Test Method.

mixin

mixin

Functionality intended to be inherited by another class as part of that class's implementation without implying specialization ("kind of" relationship) of the providing class.

"The term mixin comes from an ice cream store in Somerville, Massachusetts, where candies and cakes were mixed into the basic ice cream flavors. This seemed like a good metaphor to some of the object-oriented programmers who used to take a summer break there, especially while working with the object-oriented programming language SCOOPS" (SAMS Teach Yourself C++ in 21 Days, 4th ed., p. 458).

Functionality intended to be inherited by another class as part of that class's implementation without implying specialization ("kind of" relationship) of the providing class.

"The term mixin comes from an ice cream store in Somerville, Massachusetts, where candies and cakes were mixed into the basic ice cream flavors. This seemed like a good metaphor to some of the object-oriented programmers who used to take a summer break there, especially while working with the object-oriented programming language SCOOPS" (SAMS Teach Yourself C++ in 21 Days, 4th ed., p. 458).

module

module

In legacy programming environments (and probably a few current ones, too): An independently compilable unit of source code (e.g., the "file I/O module") that is later linked into the final executable. Unlike a component, this kind of module is typically not independently deployable. It may or may not have a corresponding set of unit tests or component tests.

When describing the functionality of a software system or application: A complete vertical chunk of the application that provides a particular piece of functionality (e.g., the "Customer Management Module") that can be used somewhat independently of the other modules. It would have a corresponding set of acceptance tests and may be the unit of incremental delivery.

In legacy programming environments (and probably a few current ones, too): An independently compilable unit of source code (e.g., the "file I/O module") that is later linked into the final executable. Unlike a component, this kind of module is typically not independently deployable. It may or may not have a corresponding set of unit tests or component tests.

When describing the functionality of a software system or application: A complete vertical chunk of the application that provides a particular piece of functionality (e.g., the "Customer Management Module") that can be used somewhat independently of the other modules. It would have a corresponding set of acceptance tests and may be the unit of incremental delivery.

need-driven development

need-driven development

A variation on the test-driven development process where code is written from the outside in and all depended-on code is replaced by Mock Objects that verify the expected indirect outputs of the code being written. This approach ensures that the responsibilities of each software unit are well understood before they are coded, by virtue of having unit tests inspired by examples of real usage. The outermost layer of software is written using storytest-driven development. It should have examples of usage by real clients (e.g., a user interface driving the Service Facade [CJ2EEP]) in addition to the customer tests.

A variation on the test-driven development process where code is written from the outside in and all depended-on code is replaced by Mock Objects that verify the expected indirect outputs of the code being written. This approach ensures that the responsibilities of each software unit are well understood before they are coded, by virtue of having unit tests inspired by examples of real usage. The outermost layer of software is written using storytest-driven development. It should have examples of usage by real clients (e.g., a user interface driving the Service Facade [CJ2EEP]) in addition to the customer tests.

object-relational mapping (ORM)

object-relational mapping (ORM)

A middleware component that translates between the object-oriented domain model of an application and the table-oriented view presented by a relational database management system.

A middleware component that translates between the object-oriented domain model of an application and the table-oriented view presented by a relational database management system.

observation point

observation point

The means by which the test observes the behavior of the system under test (SUT). This kind of interaction point can be used to inspect the post-exercise state of the SUT or to monitor interactions between the SUT and its depended-on components. Some observation points are provided strictly for the tests; they should not be used by the production code because they may expose private implementation details of the SUT that cannot be depended on not to change.

The means by which the test observes the behavior of the system under test (SUT). This kind of interaction point can be used to inspect the post-exercise state of the SUT or to monitor interactions between the SUT and its depended-on components. Some observation points are provided strictly for the tests; they should not be used by the production code because they may expose private implementation details of the SUT that cannot be depended on not to change.

ORM

ORM

See object-relational mapping.

See object-relational mapping.

outgoing interface

outgoing interface

A component (e.g., a class or a collection of classes) often depends on other components to implement its behavior. The interfaces it uses to access these components are known as outgoing interfaces, and the inputs and outputs transmitted via test interfaces are called indirect inputs and indirect outputs. Outgoing interfaces may consist of method or function calls to another component, messages sent on a message channel (e.g., MQ or JMS), or records inserted into a database or written to a file. Testing the behavior of the system under test (SUT) with outgoing interfaces requires special techniques such as Mock Objects to intercept and verify the usage of outgoing interfaces.

A component (e.g., a class or a collection of classes) often depends on other components to implement its behavior. The interfaces it uses to access these components are known as outgoing interfaces, and the inputs and outputs transmitted via test interfaces are called indirect inputs and indirect outputs. Outgoing interfaces may consist of method or function calls to another component, messages sent on a message channel (e.g., MQ or JMS), or records inserted into a database or written to a file. Testing the behavior of the system under test (SUT) with outgoing interfaces requires special techniques such as Mock Objects to intercept and verify the usage of outgoing interfaces.

pattern

pattern

A solution to a recurring problem. A pattern has a context in which it is typically applied and forces that help you choose one pattern over another based on that context. Design patterns are a particular kind of pattern. Organizational patterns are not discussed in this book.

A solution to a recurring problem. A pattern has a context in which it is typically applied and forces that help you choose one pattern over another based on that context. Design patterns are a particular kind of pattern. Organizational patterns are not discussed in this book.

pattern language

pattern language

A collection of patterns that work together to lead the reader from a very high-level problem to a very detailed solution customized for his or her particular context. When a pattern language achieves this goal, it is said to be "generative"; this characteristic differentiates a pattern language from a simple collection of patterns. Refer to "A Pattern Language for Pattern Writing" [APLfPW] to learn more about how to write a pattern language.

A collection of patterns that work together to lead the reader from a very high-level problem to a very detailed solution customized for his or her particular context. When a pattern language achieves this goal, it is said to be "generative"; this characteristic differentiates a pattern language from a simple collection of patterns. Refer to "A Pattern Language for Pattern Writing" [APLfPW] to learn more about how to write a pattern language.

polymorphism

polymorphism

Dynamic binding. The word is derived from the Latin, meaning "taking on many shapes."

Dynamic binding. The word is derived from the Latin, meaning "taking on many shapes."

presentation layer

presentation layer

The part of a Layered Architecture [DDD, PEAA, WWW] that contains the presentation logic.

The part of a Layered Architecture [DDD, PEAA, WWW] that contains the presentation logic.

presentation logic

presentation logic

The logic embedded in the presentation layer of a business system. It decides which screen to show, which items to put on menus, which items or buttons to enable or disable, and so on.

The logic embedded in the presentation layer of a business system. It decides which screen to show, which items to put on menus, which items or buttons to enable or disable, and so on.

Also known as

Also known as

function pointer, delegate (in .NET languages)

function pointer, delegate (in .NET languages)

procedure variable

procedure variable

A variable that refers to a procedure or function rather than a piece of data. It allows the code to be called to be determined at runtime (dynamic binding) rather than at compile time. The actual procedure to be invoked is assigned to the variable either during program initialization or during execution. Procedure variables were a precursor to true object-oriented programming languages (OOPLs). Early OOPLs such as C++ were built by using tables (arrays) of data structures containing procedure variables to implement the method (member function) dispatch tables for classes.

A variable that refers to a procedure or function rather than a piece of data. It allows the code to be called to be determined at runtime (dynamic binding) rather than at compile time. The actual procedure to be invoked is assigned to the variable either during program initialization or during execution. Procedure variables were a precursor to true object-oriented programming languages (OOPLs). Early OOPLs such as C++ were built by using tables (arrays) of data structures containing procedure variables to implement the method (member function) dispatch tables for classes.

production

production

In IT shops, the environment in which applications being used by real users run. This environment is distinguished from the various testing environments, such as "acceptance," "integration," "development," and "qual" (short for "quality assessment or assurance").

In IT shops, the environment in which applications being used by real users run. This environment is distinguished from the various testing environments, such as "acceptance," "integration," "development," and "qual" (short for "quality assessment or assurance").

production code

production code

In IT shops, the environment in which applications run is often called production. Production code is the code that we are writing for eventual deployment to this environment, whether the code is to be shipped in a product or deployed into "production." Compare to "test code."

In IT shops, the environment in which applications run is often called production. Production code is the code that we are writing for eventual deployment to this environment, whether the code is to be shipped in a product or deployed into "production." Compare to "test code."

programmer test

programmer test

A developer test.

A developer test.

project smell

project smell

A symptom that something has gone wrong on the project. Its underlying root cause is likely to be one or more code smells or behavior smells. Because project managers rarely run or write tests, project smells are likely the first hint they have that something may be less than perfect in test automation land.

A symptom that something has gone wrong on the project. Its underlying root cause is likely to be one or more code smells or behavior smells. Because project managers rarely run or write tests, project smells are likely the first hint they have that something may be less than perfect in test automation land.

Also known as

Also known as

pull system

pull system

pull

pull

A concept from lean manufacturing that states that things should be produced only once a real demand for them exists. In a "pull system," upstream (i.e., subcomponent) assembly lines produce only enough products to replace the items withdrawn from the pool that buffers them from the downstream assembly lines. In software development, this idea can be translated as follows: "We should only write methods that have already been called by other software and only handle those cases that the other software actually needs." This approach avoids speculation and the writing of unnecessary software, which is one of software development's key forms of inventory (which is considered waste in lean systems).

A concept from lean manufacturing that states that things should be produced only once a real demand for them exists. In a "pull system," upstream (i.e., subcomponent) assembly lines produce only enough products to replace the items withdrawn from the pool that buffers them from the downstream assembly lines. In software development, this idea can be translated as follows: "We should only write methods that have already been called by other software and only handle those cases that the other software actually needs." This approach avoids speculation and the writing of unnecessary software, which is one of software development's key forms of inventory (which is considered waste in lean systems).

red bar

red bar

Many Graphical Test Runners portray the progress of the test run using a progress bar that starts off green in color. When any tests fail, this indicator changes to a red bar.

Many Graphical Test Runners portray the progress of the test run using a progress bar that starts off green in color. When any tests fail, this indicator changes to a red bar.

refactoring

refactoring

Changing the structure of existing code without changing its behavior. Refactoring is used to improve the design of existing code, often as a first step before adding new functionality. The authoritative source for information on refactoring is Martin Fowler's book [Ref].

Changing the structure of existing code without changing its behavior. Refactoring is used to improve the design of existing code, often as a first step before adding new functionality. The authoritative source for information on refactoring is Martin Fowler's book [Ref].

reflection

reflection

The ability of a software program to examine its own structure as it is executing. Reflection is often used in software development tools to facilitate adding new capabilities.

The ability of a software program to examine its own structure as it is executing. Reflection is often used in software development tools to facilitate adding new capabilities.

regression test

regression test

A test that verifies that the behavior of a system under test (SUT) has not changed. Most regression tests are originally written as either unit tests or acceptance tests, but are subsequently included in the regression test suite to keep that functionality from being accidentally changed.

A test that verifies that the behavior of a system under test (SUT) has not changed. Most regression tests are originally written as either unit tests or acceptance tests, but are subsequently included in the regression test suite to keep that functionality from being accidentally changed.

result verification

result verification

After the exercise SUT phase of the Four-Phase Test, the test verifies that the expected (correct) outcome has actually occurred. This phase of the test is called result verification.

After the exercise SUT phase of the Four-Phase Test, the test verifies that the expected (correct) outcome has actually occurred. This phase of the test is called result verification.

Also known as

Also known as

postmortem, postpartum

postmortem, postpartum

retrospective

retrospective

A process whereby a team reviews its processes and performance for the purpose of identifying better ways of working. Retrospectives are often conducted at the end of a project (called a project retrospective) to collect data and make recommendations for future projects. They have more impact if they are done regularly during a project. Agile projects tend to do retrospectives after at least every release (called a release retrospective) and often after every iteration (called an iteration retrospective.)

A process whereby a team reviews its processes and performance for the purpose of identifying better ways of working. Retrospectives are often conducted at the end of a project (called a project retrospective) to collect data and make recommendations for future projects. They have more impact if they are done regularly during a project. Agile projects tend to do retrospectives after at least every release (called a release retrospective) and often after every iteration (called an iteration retrospective.)

root cause analysis

root cause analysis

A process wherein the cause of a failure or bug is traced back to all possible contributing factors. A root cause analysis helps us avoid treating symptoms by identifying the true sources of our problems. A number of techniques for doing root cause analysis exist, including Toyota's "five why's" [TPS].

A process wherein the cause of a failure or bug is traced back to all possible contributing factors. A root cause analysis helps us avoid treating symptoms by identifying the true sources of our problems. A number of techniques for doing root cause analysis exist, including Toyota's "five why's" [TPS].

round-trip test

round-trip test

A test that interacts only via the "front door" (public interface) of the system under test (SUT). Compare with layer-crossing test.

A test that interacts only via the "front door" (public interface) of the system under test (SUT). Compare with layer-crossing test.

service object

service object

An object that provides a service to other objects. Service objects typically do not have a life cycle of their own; any state they do contain tends to be an aggregate of the states of the entity objects that they vend. The interface of a service object is often defined via a Service Facade [CJ2EEP] class. EJB Session Beans are one example of a service object.

An object that provides a service to other objects. Service objects typically do not have a life cycle of their own; any state they do contain tends to be an aggregate of the states of the entity objects that they vend. The interface of a service object is often defined via a Service Facade [CJ2EEP] class. EJB Session Beans are one example of a service object.

Also known as

Also known as

service component

service component

setter

setter

A method provided by an object specifically to set the value of one of its attributes. By convention, it either has the same name as the attribute or its name includes the prefix "set" (e.g., setName).

A method provided by an object specifically to set the value of one of its attributes. By convention, it either has the same name as the attribute or its name includes the prefix "set" (e.g., setName).

smell

smell

A symptom of a problem. A smell doesn't necessarily tell us what is wrong, because it may have several possible causes. A smell must pass the "sniffability test"—that is, it must grab us by the nose and say, "Something is wrong here." To figure out exactly what the smell means, we must perform root cause analysis.

We classify smells based on where we find them. The most common kinds are (production) code smells, test smells, and project smells. Test smells may be either (test) code smells or behavior smells.

A symptom of a problem. A smell doesn't necessarily tell us what is wrong, because it may have several possible causes. A smell must pass the "sniffability test"—that is, it must grab us by the nose and say, "Something is wrong here." To figure out exactly what the smell means, we must perform root cause analysis.

We classify smells based on where we find them. The most common kinds are (production) code smells, test smells, and project smells. Test smells may be either (test) code smells or behavior smells.

spike

spike

In agile methods such as eXtreme Programming, a time-boxed experiment used to obtain enough information to estimate the effort required to implement a new kind of functionality.

In agile methods such as eXtreme Programming, a time-boxed experiment used to obtain enough information to estimate the effort required to implement a new kind of functionality.

stateless

stateless

An object that does not maintain any state between invocations of its operations. That is, each request is self-contained and does not require that the same server object be used for a series of requests.

An object that does not maintain any state between invocations of its operations. That is, each request is self-contained and does not require that the same server object be used for a series of requests.

static binding

static binding

Resolving exactly which piece of software we will transfer control to at compile time. Static binding is the opposite of dynamic binding.

Resolving exactly which piece of software we will transfer control to at compile time. Static binding is the opposite of dynamic binding.

static method

static method

In Java, a method that the compiler resolves at compile time (rather than at runtime using dynamic binding). This behavior is the opposite of dynamic (or virtual in C++). A static method is also a class method because only class methods can be resolved at compile time in Java. A static method is not necessarily a class method in all languages, however. For example:

In Java, a method that the compiler resolves at compile time (rather than at runtime using dynamic binding). This behavior is the opposite of dynamic (or virtual in C++). A static method is also a class method because only class methods can be resolved at compile time in Java. A static method is not necessarily a class method in all languages, however. For example:

Assert.assertEquals(message,  expected,  actual);

Assert.assertEquals(message,  expected,  actual);

 

static variable

static variable

In Java, a variable (field) that the compiler resolves at compile time rather than at runtime using dynamic binding. A static variable is also a class variable because only class variables can be resolved at compile time in Java. Being static (i.e., not dynamic) does not necessarily imply that something is associated with a class (rather than an instance) in all languages.

In Java, a variable (field) that the compiler resolves at compile time rather than at runtime using dynamic binding. A static variable is also a class variable because only class variables can be resolved at compile time in Java. Being static (i.e., not dynamic) does not necessarily imply that something is associated with a class (rather than an instance) in all languages.

STDD

STDD

See storytest-driven development.

See storytest-driven development.

story

story

See user story.

See user story.

storytest

storytest

A customer test that is the "confirmation" part of the user story "trilogy": card, conversation, confirmation [XPC]. When the storytests are written before any software is developed, we call the process storytest-driven development.

A customer test that is the "confirmation" part of the user story "trilogy": card, conversation, confirmation [XPC]. When the storytests are written before any software is developed, we call the process storytest-driven development.

storytest-driven development (STDD)

storytest-driven development (STDD)

A variation of the test-driven development process that entails writing (and usually automating) customer tests before the development of the corresponding functionality begins. This approach ensures that integration of the various software units verified by the unit tests results in a usable whole. The term "storytest-driven development" was first coined by Joshua Kerievsky as part of his methodology "Industrial XP" [IXP].

A variation of the test-driven development process that entails writing (and usually automating) customer tests before the development of the corresponding functionality begins. This approach ensures that integration of the various software units verified by the unit tests results in a usable whole. The term "storytest-driven development" was first coined by Joshua Kerievsky as part of his methodology "Industrial XP" [IXP].

STTCPW

STTCPW

"The simplest thing that could possibly work." This approach is commonly used on XP projects when someone is over-engineering the software by trying to anticipate future requirements.

"The simplest thing that could possibly work." This approach is commonly used on XP projects when someone is over-engineering the software by trying to anticipate future requirements.

substitutable dependency

substitutable dependency

A software component may depend on any number of other components. If we are to test this component by itself, we must be able to replace the other components with Test Doubles—that is, each component must be a substitutable dependency. We can turn something into a substitutable dependency in several ways, including Dependency Injection, Dependency Lookup, and Test-Specific Subclass.

A software component may depend on any number of other components. If we are to test this component by itself, we must be able to replace the other components with Test Doubles—that is, each component must be a substitutable dependency. We can turn something into a substitutable dependency in several ways, including Dependency Injection, Dependency Lookup, and Test-Specific Subclass.

synchronous test

synchronous test

A test that interacts with the system under test (SUT) using normal (synchronous) method calls that return the results that the test will make assertions against. A synchronous test does not need to coordinate its steps with those of the SUT; this activity is managed automatically by the runtime system. Contrast this with an asynchronous test, which runs in a separate thread of control from the SUT.

A test that interacts with the system under test (SUT) using normal (synchronous) method calls that return the results that the test will make assertions against. A synchronous test does not need to coordinate its steps with those of the SUT; this activity is managed automatically by the runtime system. Contrast this with an asynchronous test, which runs in a separate thread of control from the SUT.

Also known as

Also known as

AUT, CUT, MUT, OUT

AUT, CUT, MUT, OUT

system under test (SUT)

system under test (SUT)

Whatever thing we are testing. The SUT is always defined from the perspective of the test. When we are writing unit tests, the SUT is whatever class (also known as CUT), object (also known as OUT), or method (also known as MUT) we are testing; when we are writing customer tests, the SUT is probably the entire application (also known as AUT) or at least a major subsystem of it. The parts of the application that we are not verifying in this particular test may still be involved as a depended-on component (DOC).

Whatever thing we are testing. The SUT is always defined from the perspective of the test. When we are writing unit tests, the SUT is whatever class (also known as CUT), object (also known as OUT), or method (also known as MUT) we are testing; when we are writing customer tests, the SUT is probably the entire application (also known as AUT) or at least a major subsystem of it. The parts of the application that we are not verifying in this particular test may still be involved as a depended-on component (DOC).

task

task

The unit of work assignment (or volunteering) in eXtreme Programming. One or more tasks may be involved in delivering a user story (a feature).

The unit of work assignment (or volunteering) in eXtreme Programming. One or more tasks may be involved in delivering a user story (a feature).

TDD

TDD

See test-driven development.

See test-driven development.

test

test

A procedure, whether manually executed or automated, that can be used to verify that the system under test (SUT) is behaving as expected. Often called a test case.

A procedure, whether manually executed or automated, that can be used to verify that the system under test (SUT) is behaving as expected. Often called a test case.

test automater

test automater

The person or project role that is responsible for building the tests. Sometimes a "subject matter expert" may be responsible for coming up with the tests to be automated by the test automater.

The person or project role that is responsible for building the tests. Sometimes a "subject matter expert" may be responsible for coming up with the tests to be automated by the test automater.

test case

test case

Usually a synonym for "test." In xUnit, it may also refer to a Testcase Class, which is actually a Test Suite Factory as well as a place to put a set of related Test Methods.

Usually a synonym for "test." In xUnit, it may also refer to a Testcase Class, which is actually a Test Suite Factory as well as a place to put a set of related Test Methods.

test code

test code

Code written specifically to test other code (either production or other test code).

Code written specifically to test other code (either production or other test code).

test condition

test condition

A particular behavior of the system under test (SUT) that we need to verify. It can be described as the following collection of points:

A particular behavior of the system under test (SUT) that we need to verify. It can be described as the following collection of points:

  • If the SUT is in some state S1, and
  • If the SUT is in some state S1, and
  • We exercise the SUT in some way X, then
  • We exercise the SUT in some way X, then
  • The SUT should respond with R and
  • The SUT should respond with R and
  • The SUT should be in state S2.
  • The SUT should be in state S2.

test context

test context

Everything a system under test (SUT) needs to have in place so that we can exercise the SUT for the purpose of verifying its behavior. For this reason, RSpec calls the test fixture (as used in xUnit) a "context."

Everything a system under test (SUT) needs to have in place so that we can exercise the SUT for the purpose of verifying its behavior. For this reason, RSpec calls the test fixture (as used in xUnit) a "context."

Context:  a  set  fruits  with

      contents  =  {apple,  orange,  pear}

Exercise:  remove  orange  from  the  fruits  set

Verify:  fruits  set  contents  =  {apple,  pear}

Context:  a  set  fruits  with

      contents  =  {apple,  orange,  pear}

Exercise:  remove  orange  from  the  fruits  set

Verify:  fruits  set  contents  =  {apple,  pear}

 

In this example, the fixture consists of a single set and is created directly in the test. How we choose to construct the fixture has very far-reaching ramifications for all aspects of test writing and maintenance.

In this example, the fixture consists of a single set and is created directly in the test. How we choose to construct the fixture has very far-reaching ramifications for all aspects of test writing and maintenance.

test database

test database

A database instance that is used primarily for the execution of tests. It should not be the same database as is used in production!

A database instance that is used primarily for the execution of tests. It should not be the same database as is used in production!

test debt

test debt

I first became aware of the concept of various kinds of debts via the Industrial XP mailing list on the Internet. The concept of "debt" is a metaphor for "not doing enough of" something. To get out of debt, we must put extra effort into the something we were not doing enough of. Test debt arises when we do not write all of the necessary tests. As a result, we have "unprotected code" in that the code could break without causing any tests to fail.

I first became aware of the concept of various kinds of debts via the Industrial XP mailing list on the Internet. The concept of "debt" is a metaphor for "not doing enough of" something. To get out of debt, we must put extra effort into the something we were not doing enough of. Test debt arises when we do not write all of the necessary tests. As a result, we have "unprotected code" in that the code could break without causing any tests to fail.

test-driven bug fixing

test-driven bug fixing

A way of fixing bugs that entails writing and automating unit tests that reproduce each bug before we begin debugging the code and fixing the bug; the bug-fixing extension of test-driven development.

A way of fixing bugs that entails writing and automating unit tests that reproduce each bug before we begin debugging the code and fixing the bug; the bug-fixing extension of test-driven development.

test-driven development (TDD)

test-driven development (TDD)

A development process that entails writing and automating unit tests before the development of the corresponding units begins. TDD ensures that the responsibilities of each software unit are well understood before they are coded. Unlike test-first development, test-driven development is typically meant to imply that the production code is made to work one test at a time (a characteristic called emergent design).

See also: storytest-driven development.

A development process that entails writing and automating unit tests before the development of the corresponding units begins. TDD ensures that the responsibilities of each software unit are well understood before they are coded. Unlike test-first development, test-driven development is typically meant to imply that the production code is made to work one test at a time (a characteristic called emergent design).

See also: storytest-driven development.

test driver

test driver

A person doing test-driven development.

A person doing test-driven development.

test driving

test driving

The act of doing test-driven development.

The act of doing test-driven development.

test error

test error

When a test is run, an error that keeps the test from running to completion. The error may be explicitly raised or thrown by the system under test (SUT) or by the test itself, or it may be thrown by the runtime system (e.g., operating system, virtual machine). In general, it is much easier to debug a test error than a test failure because the cause of the problem tends to be much more local to where the test error occurs. Compare with test failure and test success.

When a test is run, an error that keeps the test from running to completion. The error may be explicitly raised or thrown by the system under test (SUT) or by the test itself, or it may be thrown by the runtime system (e.g., operating system, virtual machine). In general, it is much easier to debug a test error than a test failure because the cause of the problem tends to be much more local to where the test error occurs. Compare with test failure and test success.

test failure

test failure

When a test is run and the actual outcome does not match the expected outcome. Compare with test error and test success.

When a test is run and the actual outcome does not match the expected outcome. Compare with test error and test success.

test-first development

test-first development

A development process that entails writing and automating unit tests before the development of the corresponding units begins. Test-first development ensures that the responsibilities of each software unit are well understood before that unit is coded. Unlike test-driven development, test-first development merely says that the tests are written before the production code; it does not imply that the production code is made to work one test at a time (emergent design). Test-first development may be applied at the unit test or customer test level, depending on which tests we have chosen to automate.

A development process that entails writing and automating unit tests before the development of the corresponding units begins. Test-first development ensures that the responsibilities of each software unit are well understood before that unit is coded. Unlike test-driven development, test-first development merely says that the tests are written before the production code; it does not imply that the production code is made to work one test at a time (emergent design). Test-first development may be applied at the unit test or customer test level, depending on which tests we have chosen to automate.

test fixture (disambiguation)

test fixture (disambiguation)

In generic xUnit: All the things we need to have in place to run a test and expect a particular outcome. The test fixture comprises the pre-conditions of the test; that is, it is the "before" picture of the SUT and its context. See also: test fixture (in xUnit) and test context.

In NUnit and VbUnit: The Testcase Class. See also: test fixture (in NUnit).

In Fit: The adapter that interprets the Fit table and invokes methods on the system under test (SUT), thereby implementing a Data-Driven Test.

See also: fixture (Fit).

In generic xUnit: All the things we need to have in place to run a test and expect a particular outcome. The test fixture comprises the pre-conditions of the test; that is, it is the "before" picture of the SUT and its context. See also: test fixture (in xUnit) and test context.

In NUnit and VbUnit: The Testcase Class. See also: test fixture (in NUnit).

In Fit: The adapter that interprets the Fit table and invokes methods on the system under test (SUT), thereby implementing a Data-Driven Test.

See also: fixture (Fit).

test fixture (in NUnit)

test fixture (in NUnit)

In NUnit (and in VbUnit and most .NET implementations of xUnit): The Testcase Class on which the Test Methods are implemented. We add the attribute [TestFixture] to the class that hosts the Test Methods.

Some members of the xUnit family assume that an instance of the Testcase Class "is a" test context; NUnit is a good example. This interpretation assumes we are using the Testcase Class per Fixture approach to organizing the tests. When we choose to use a different way of organizing the tests, such as Testcase Class per Class or Testcase Class per Feature, this merging of the concepts of test context and Testcase Class can be confusing. This book uses "test fixture" to mean "the pre-conditions of the test" (also known as the test context) and Testcase Class to mean "the class that contains the Test Methods and any code needed to set up the test context."

In NUnit (and in VbUnit and most .NET implementations of xUnit): The Testcase Class on which the Test Methods are implemented. We add the attribute [TestFixture] to the class that hosts the Test Methods.

Some members of the xUnit family assume that an instance of the Testcase Class "is a" test context; NUnit is a good example. This interpretation assumes we are using the Testcase Class per Fixture approach to organizing the tests. When we choose to use a different way of organizing the tests, such as Testcase Class per Class or Testcase Class per Feature, this merging of the concepts of test context and Testcase Class can be confusing. This book uses "test fixture" to mean "the pre-conditions of the test" (also known as the test context) and Testcase Class to mean "the class that contains the Test Methods and any code needed to set up the test context."

Also known as

Also known as

Testcase Class

Testcase Class

test fixture (in xUnit)

test fixture (in xUnit)

In xUnit: All the things we need to have in place to run a test and expect a particular outcome (i.e., the test context. Some variants of xUnit keep the concept of the test context separate from the Testcase Class that creates it; JUnit and its direct ports fall into this camp. Setting up the test fixture is the first phase of the Four-Phase Test. For meanings of the term "test fixture" in other contexts, see test fixture (disambiguation).

In xUnit: All the things we need to have in place to run a test and expect a particular outcome (i.e., the test context. Some variants of xUnit keep the concept of the test context separate from the Testcase Class that creates it; JUnit and its direct ports fall into this camp. Setting up the test fixture is the first phase of the Four-Phase Test. For meanings of the term "test fixture" in other contexts, see test fixture (disambiguation).

Also known as

Also known as

test context

test context

test-last development

test-last development

A development process that entails executing unit tests after the development of the corresponding units is finished. Unlike test-first development, test-last development merely says that testing should be done before the code goes into production; it does not imply that the tests are automated. Traditional QA (quality assurance) testing is inherently test-last development unless the tests are prepared as part of the requirements phase of the project and are shared with the development team.

A development process that entails executing unit tests after the development of the corresponding units is finished. Unlike test-first development, test-last development merely says that testing should be done before the code goes into production; it does not imply that the tests are automated. Traditional QA (quality assurance) testing is inherently test-last development unless the tests are prepared as part of the requirements phase of the project and are shared with the development team.

test maintainer

test maintainer

The person or project role responsible for maintaining the tests as the system or application evolves. Most commonly, this person is enhancing the system with new functionality or fixing bugs. The test maintainer could also be whoever is called in when the automated tests fail for whatever reason. If the test maintainer is doing the enhancements by writing tests first, he or she is also a test driver.

The person or project role responsible for maintaining the tests as the system or application evolves. Most commonly, this person is enhancing the system with new functionality or fixing bugs. The test maintainer could also be whoever is called in when the automated tests fail for whatever reason. If the test maintainer is doing the enhancements by writing tests first, he or she is also a test driver.

test package

test package

In languages that provide packages or namespaces, a package or name that exists for the purpose of hosting Testcase Classes.

In languages that provide packages or namespaces, a package or name that exists for the purpose of hosting Testcase Classes.

test reader

test reader

Anyone who has reason to read tests, including a test maintainer or test driver. This individual may be reading the tests primarily for the purpose of understanding what the system under test (SUT) is supposed to do (Tests as Documentation) or as part of a test maintenance or software development activity.

Anyone who has reason to read tests, including a test maintainer or test driver. This individual may be reading the tests primarily for the purpose of understanding what the system under test (SUT) is supposed to do (Tests as Documentation) or as part of a test maintenance or software development activity.

test result

test result

A test or test suite can be run many times, each time yielding a different test result.

A test or test suite can be run many times, each time yielding a different test result.

test run

test run

A test or test suite can be run many times, each time yielding a different test result. Some commercial test automation tools record the results of each test run for prosperity.

A test or test suite can be run many times, each time yielding a different test result. Some commercial test automation tools record the results of each test run for prosperity.

test smell

test smell

A symptom of a problem in test code. A smell doesn't necessarily tell us what is wrong because it may have several possible causes. Like all smells, a test smell must pass the "sniffability test"—that is, it must grab us by the nose and say, "Something is wrong here."

A symptom of a problem in test code. A smell doesn't necessarily tell us what is wrong because it may have several possible causes. Like all smells, a test smell must pass the "sniffability test"—that is, it must grab us by the nose and say, "Something is wrong here."

test-specific equality

test-specific equality

Tests and the system under test (SUT) may have different ideas about what constitutes equality of two objects. In fact, this understanding may differ from one test to another. It is not advisable to modify the definition of equality within the SUT to match the tests' expectations, as this practice leads to Equality Pollution. Making individual Equality Assertions on many attributes of an object is not the answer either, as it can result in Obscure Tests and Test Code Duplication. Instead, build one or more Custom Assertions that meets your tests' needs.

Tests and the system under test (SUT) may have different ideas about what constitutes equality of two objects. In fact, this understanding may differ from one test to another. It is not advisable to modify the definition of equality within the SUT to match the tests' expectations, as this practice leads to Equality Pollution. Making individual Equality Assertions on many attributes of an object is not the answer either, as it can result in Obscure Tests and Test Code Duplication. Instead, build one or more Custom Assertions that meets your tests' needs.

test stripper

test stripper

A step or program in the build process that removes all the test code from the compiled and linked executable.

A step or program in the build process that removes all the test code from the compiled and linked executable.

test success

test success

A situation in which a test is run and all actual outcomes match the expected outcomes. Compare with test failure and test error.

A situation in which a test is run and all actual outcomes match the expected outcomes. Compare with test failure and test error.

test suite

test suite

A way to name a collection of tests that we want to run together.

A way to name a collection of tests that we want to run together.

Unified Modeling Language (UML)

Unified Modeling Language (UML)

From Wikipedia [Wp]: "[A] nonproprietary specification language for object modeling. UML is a general-purpose modeling language that includes a standardized graphical notation used to create an abstract model of a system, referred to as a UML model."

From Wikipedia [Wp]: "[A] nonproprietary specification language for object modeling. UML is a general-purpose modeling language that includes a standardized graphical notation used to create an abstract model of a system, referred to as a UML model."

unit test

unit test

A test that verifies the behavior of some small part of the overall system. What turns a test into a unit test is that the system under test (SUT) is a very small subset of the overall system and may be unrecognizable to someone who is not involved in building the software. The actual SUT may be as small as a single object or method that is a consequence of one or more design decisions, although its behavior may also be traced back to some aspect of the functional requirements. Unit tests need not be readable, recognizable, or verifiable by the customer or business domain expert. Contrast this with a customer test, which is derived almost entirely from the requirements and which should be verifiable by the customer. In eXtreme Programming, unit tests are also called developer tests or programmer tests.

A test that verifies the behavior of some small part of the overall system. What turns a test into a unit test is that the system under test (SUT) is a very small subset of the overall system and may be unrecognizable to someone who is not involved in building the software. The actual SUT may be as small as a single object or method that is a consequence of one or more design decisions, although its behavior may also be traced back to some aspect of the functional requirements. Unit tests need not be readable, recognizable, or verifiable by the customer or business domain expert. Contrast this with a customer test, which is derived almost entirely from the requirements and which should be verifiable by the customer. In eXtreme Programming, unit tests are also called developer tests or programmer tests.

use case

use case

A way of describing the functionality of a system in terms of what its users are trying to achieve and what the system needs to do to achieve their goals. Unlike user stories, use cases may cover many different scenarios yet are often not testable independently.

A way of describing the functionality of a system in terms of what its users are trying to achieve and what the system needs to do to achieve their goals. Unlike user stories, use cases may cover many different scenarios yet are often not testable independently.

user acceptance test (UAT)

user acceptance test (UAT)

See acceptance test.

See acceptance test.

user story

user story

The unit of incremental development in eXtreme Programming. We must INVEST in good user stories—that is, each user story must be Independent, Negotiable, Valuable, Estimatable, Small, and Testable [XP123]. A user story corresponds roughly to a "feature" in non-eXtreme Programming terminology and is typically decomposed into one or more tasks to be carried out by project team members.

The unit of incremental development in eXtreme Programming. We must INVEST in good user stories—that is, each user story must be Independent, Negotiable, Valuable, Estimatable, Small, and Testable [XP123]. A user story corresponds roughly to a "feature" in non-eXtreme Programming terminology and is typically decomposed into one or more tasks to be carried out by project team members.

Also known as

Also known as

story, feature

story, feature

verify outcome

verify outcome

After the exercise SUT phase of the test, the test compares the actual outcome—including returned values, indirect outputs, and the post-test state of the system under test (SUT)—with the expected outcome. This phase of the test is called the verify outcome phase.

After the exercise SUT phase of the test, the test compares the actual outcome—including returned values, indirect outputs, and the post-test state of the system under test (SUT)—with the expected outcome. This phase of the test is called the verify outcome phase.

References

References

 

[AP]

[AP]

AntiPatterns: Refactoring Software, Architectures, and Projects in Crisis

AntiPatterns: Refactoring Software, Architectures, and Projects in Crisis

Published by: John Wiley (1998)

Published by: John Wiley (1998)

ISBN: 0-471-19713-0

ISBN: 0-471-19713-0

By: William J. Brown et al.

By: William J. Brown et al.

This book describes common problems on software projects and suggests how to eliminate them by changing the architecture or project organization.

This book describes common problems on software projects and suggests how to eliminate them by changing the architecture or project organization.

 

[APLfPW]

[APLfPW]

A Pattern Language for Pattern Writing

A Pattern Language for Pattern Writing

In: Pattern Languages of Program Design 3 [PLoPD3], pp. 529–574.

In: Pattern Languages of Program Design 3 [PLoPD3], pp. 529–574.

Published by: Addison-Wesley (1998)

Published by: Addison-Wesley (1998)

By: Gerard Meszaros and James Doble

By: Gerard Meszaros and James Doble

As the patterns community has accumulated experience in writing and reviewing patterns and pattern languages, we have begun to develop insight into pattern-writing techniques and approaches that have been observed to be particularly effective at addressing certain recurring problems. This pattern language attempts to capture some of these "best practices" of pattern writing, both by describing them in pattern form and by demonstrating them in action. As such, this pattern language is its own running example.

As the patterns community has accumulated experience in writing and reviewing patterns and pattern languages, we have begun to develop insight into pattern-writing techniques and approaches that have been observed to be particularly effective at addressing certain recurring problems. This pattern language attempts to capture some of these "best practices" of pattern writing, both by describing them in pattern form and by demonstrating them in action. As such, this pattern language is its own running example.

 

Further Reading

Further Reading

Full text of this paper is available online in PDF form at http://PatternWritingPatterns.gerardmeszaros.com and in HTML form, complete with a hyperlinked table of contents, at http://hillside.net/patterns/writing/patternwritingpaper.htm.

Full text of this paper is available online in PDF form at http://PatternWritingPatterns.gerardmeszaros.com and in HTML form, complete with a hyperlinked table of contents, at http://hillside.net/patterns/writing/patternwritingpaper.htm.

 

[ARTRP]

[ARTRP]

Agile Regression Testing Using Record and Playback

Agile Regression Testing Using Record and Playback

http://AgileRegressionTestPaper.gerardmeszaros.com

http://AgileRegressionTestPaper.gerardmeszaros.com

By: Gerard Meszaros and Ralph Bohnet

By: Gerard Meszaros and Ralph Bohnet

This paper was presented at XP/Agile Universe 2003. It describes how we built a "record and playback" test mechanism into a safety-critical application to make it easier to regression test it as it was ported from OS2 to Windows.

This paper was presented at XP/Agile Universe 2003. It describes how we built a "record and playback" test mechanism into a safety-critical application to make it easier to regression test it as it was ported from OS2 to Windows.

 

[CJ2EEP]

[CJ2EEP]

Core J2EE™ Patterns, Second Edition: Best Practices and Design Strategies

Core J2EE™ Patterns, Second Edition: Best Practices and Design Strategies

Published by: Prentice Hall (2003)

Published by: Prentice Hall (2003)

ISBN: 0-131-42246-4

ISBN: 0-131-42246-4

By: Deepak Alur, Dan Malks, and John Crupi

By: Deepak Alur, Dan Malks, and John Crupi

This book catalogs the core patterns of usage of Enterprise Java Beans (EJB), which are a key part of the Java 2 Enterprise Edition. Examples include Session Facade [CJ2EEP].

This book catalogs the core patterns of usage of Enterprise Java Beans (EJB), which are a key part of the Java 2 Enterprise Edition. Examples include Session Facade [CJ2EEP].

 

[DDD]

[DDD]

Domain-Driven Design: Tackling Complexity in the Heart of Software

Domain-Driven Design: Tackling Complexity in the Heart of Software

Published by: Addison-Wesley (2004)

Published by: Addison-Wesley (2004)

ISBN: 0-321-12521-5

ISBN: 0-321-12521-5

By: Eric Evans

By: Eric Evans

This book is a good introduction to the process of using a domain model as the heart of a software system.

This book is a good introduction to the process of using a domain model as the heart of a software system.

 

Readers learn how to use a domain model to make complex development effort more focused and dynamic. A core of best practices and standard patterns provides a common language for the development team.

Readers learn how to use a domain model to make complex development effort more focused and dynamic. A core of best practices and standard patterns provides a common language for the development team.

 

[ET]

[ET]

Endo-Testing

Endo-Testing

http://www.connextra.com/aboutUs/mockobjects.pdf

http://www.connextra.com/aboutUs/mockobjects.pdf

By: Tim Mackinnon, Steve Freeman, and Philip Craig

By: Tim Mackinnon, Steve Freeman, and Philip Craig

This paper, which was presented at XP 2000 in Sardinia, describes the use of Mock Objects (page 544) to facilitate testing of the behavior of an object by monitoring its behavior while it is executing.

This paper, which was presented at XP 2000 in Sardinia, describes the use of Mock Objects (page 544) to facilitate testing of the behavior of an object by monitoring its behavior while it is executing.

 

Unit testing is a fundamental practice in eXtreme Programming, but most nontrivial code is difficult to test in isolation. It is hard to avoid writing test suites that are complex, incomplete, and difficult to maintain and interpret. Using Mock Objects for unit testing improves both domain code and test suites. These objects allow unit tests to be written for everything, simplify test structure, and avoid polluting domain code with testing infrastructure.

Unit testing is a fundamental practice in eXtreme Programming, but most nontrivial code is difficult to test in isolation. It is hard to avoid writing test suites that are complex, incomplete, and difficult to maintain and interpret. Using Mock Objects for unit testing improves both domain code and test suites. These objects allow unit tests to be written for everything, simplify test structure, and avoid polluting domain code with testing infrastructure.

 

[FaT]

[FaT]

Frameworks and Testing

Frameworks and Testing

In: Proceedings of XP2002

In: Proceedings of XP2002

http://www.agilealliance.org/articles/roockstefanframeworks/file

http://www.agilealliance.org/articles/roockstefanframeworks/file

By: Stefan Roock

By: Stefan Roock

This paper is mandatory reading for framework builders. It describes four kinds of automated testing that should accompany a framework, including the ability to test a plug-in's compliance with the framework's protocol and a testing framework that makes it easier to test applications built on the framework.

This paper is mandatory reading for framework builders. It describes four kinds of automated testing that should accompany a framework, including the ability to test a plug-in's compliance with the framework's protocol and a testing framework that makes it easier to test applications built on the framework.

 

[FitB]

[FitB]

Fit for Developing Software

Fit for Developing Software

Published by: Addison-Wesley (2005)

Published by: Addison-Wesley (2005)

ISBN: 0-321-26934-9

ISBN: 0-321-26934-9

By: Rick Mugridge and Ward Cunningham

By: Rick Mugridge and Ward Cunningham

This book is a great introduction to the use of Data-Driven Tests (page 288) for preparing customer tests, whether as part of agile or traditional projects. This is what I wrote for inclusion as "advance praise":

This book is a great introduction to the use of Data-Driven Tests (page 288) for preparing customer tests, whether as part of agile or traditional projects. This is what I wrote for inclusion as "advance praise":

 

Wow! This is the book I wish I had on my desk when I did my first storytest-driven development project. It explains the philosophy behind the Fit framework and a process for using it to interact with the customers to help define the requirements of the project. It makes Fit so easy and approachable that I wrote my first FitNesse tests before I even I finished the book.

Wow! This is the book I wish I had on my desk when I did my first storytest-driven development project. It explains the philosophy behind the Fit framework and a process for using it to interact with the customers to help define the requirements of the project. It makes Fit so easy and approachable that I wrote my first FitNesse tests before I even I finished the book.

 

Further Reading

Further Reading

More information on Fit can be found at Ward's Web site, http://fit.c2.com.

More information on Fit can be found at Ward's Web site, http://fit.c2.com.

[GOF]

[GOF]

Design Patterns: Elements of Reusable Object-Oriented Software

Design Patterns: Elements of Reusable Object-Oriented Software

Published by: Addison-Wesley (1995)

Published by: Addison-Wesley (1995)

ISBN: 0-201-63361-2

ISBN: 0-201-63361-2

By: Erich Gamma, Richard Helm, Ralph Johnson, and John M.Vlissides

By: Erich Gamma, Richard Helm, Ralph Johnson, and John M.Vlissides

This book started the patterns movement. In it, the "Gang of Four" describe 23 recurring patterns in object-oriented software systems. Examples include Composite [GOF], Factory Method [GOF], and Facade [GOF]

This book started the patterns movement. In it, the "Gang of Four" describe 23 recurring patterns in object-oriented software systems. Examples include Composite [GOF], Factory Method [GOF], and Facade [GOF]

 

[HoN]

[HoN]

Hierarchy of Needs

Hierarchy of Needs

From Wikipedia [Wp]:

From Wikipedia [Wp]:

 

Maslow's hierarchy of needs is a theory in psychology that Abraham Maslow proposed in his 1943 paper "A Theory of Human Motivation," which he subsequently extended. His theory contends that as humans meet "basic needs," they seek to satisfy successively "higher needs" that occupy a set hierarchy. . . .

Maslow's hierarchy of needs is a theory in psychology that Abraham Maslow proposed in his 1943 paper "A Theory of Human Motivation," which he subsequently extended. His theory contends that as humans meet "basic needs," they seek to satisfy successively "higher needs" that occupy a set hierarchy. . . .

 

Maslow's hierarchy of needs is often depicted as a pyramid consisting of five levels: The four lower levels are grouped together as deficiency needs associated with physiological needs, while the top level is termed growth needs associated with psychological needs. While our deficiency needs must be met, our being needs are continually shaping our behavior. The basic concept is that the higher needs in this hierarchy only come into focus once all the needs that are lower down in the pyramid are mainly or entirely satisfied. Growth forces create upward movement in the hierarchy, whereas regressive forces push prepotent needs farther down the hierarchy.

Maslow's hierarchy of needs is often depicted as a pyramid consisting of five levels: The four lower levels are grouped together as deficiency needs associated with physiological needs, while the top level is termed growth needs associated with psychological needs. While our deficiency needs must be met, our being needs are continually shaping our behavior. The basic concept is that the higher needs in this hierarchy only come into focus once all the needs that are lower down in the pyramid are mainly or entirely satisfied. Growth forces create upward movement in the hierarchy, whereas regressive forces push prepotent needs farther down the hierarchy.

 

[IEAT]

[IEAT]

Improving the Effectiveness of Automated Tests

Improving the Effectiveness of Automated Tests

http://FasterTestsPaper.gerardmeszaros.com.

http://FasterTestsPaper.gerardmeszaros.com.

By: Gerard Meszaros, Shaun Smith, and Jennitta Andrea

By: Gerard Meszaros, Shaun Smith, and Jennitta Andrea

This paper was presented at XP2001 in Sardinia, Italy. It describes a number of issues that reduce the speed and effectiveness of automated unit tests and suggests ways to address them.

This paper was presented at XP2001 in Sardinia, Italy. It describes a number of issues that reduce the speed and effectiveness of automated unit tests and suggests ways to address them.

 

[IXP]

[IXP]

Industrial XP

Industrial XP

http://ixp.industriallogic.com.

http://ixp.industriallogic.com.

Industrial XP is a "branded" variant of eXtreme Programming created by Joshua Kerievsky of Industrial Logic. It includes a number of practices required to scale eXtreme Programming to work in larger enterprises, such as "Project Chartering."

Industrial XP is a "branded" variant of eXtreme Programming created by Joshua Kerievsky of Industrial Logic. It includes a number of practices required to scale eXtreme Programming to work in larger enterprises, such as "Project Chartering."

 

[JBrains]

[JBrains]

JetBrains

JetBrains

http://www.jetbrains.com.

http://www.jetbrains.com.

JetBrains builds software development tools that automate (among other things) refactoring. Its Web site contains a list of all refactorings that the company's various tools support, including some that are not described in [Ref].

JetBrains builds software development tools that automate (among other things) refactoring. Its Web site contains a list of all refactorings that the company's various tools support, including some that are not described in [Ref].

[JNI]

[JNI]

JUnit New Instance

JUnit New Instance

http://www.martinfowler.com/bliki/JunitNewInstance.html

http://www.martinfowler.com/bliki/JunitNewInstance.html

This article by Martin Fowler provides the background for why it makes sense for JUnit and many of its ports to create a new instance of the Testcase Class (page 373) for each Test Method (page 348).

This article by Martin Fowler provides the background for why it makes sense for JUnit and many of its ports to create a new instance of the Testcase Class (page 373) for each Test Method (page 348).

 

[JuPG]

[JuPG]

JUnit Pocket Guide

JUnit Pocket Guide

Published by: O'Reilly

Published by: O'Reilly

ISBN: 0-596-00743-4

ISBN: 0-596-00743-4

By: Kent Beck

By: Kent Beck

This 80-page, small-format book is an excellent summary of key features of JUnit and best practices for writing tests. Being small enough to fit in a pocket, it doesn't go into much detail, but it does give us an idea of what is possible and where to look for details.

This 80-page, small-format book is an excellent summary of key features of JUnit and best practices for writing tests. Being small enough to fit in a pocket, it doesn't go into much detail, but it does give us an idea of what is possible and where to look for details.

[LSD]

[LSD]

Lean Software Development: An Agile Toolkit

Lean Software Development: An Agile Toolkit

Published by: Addison-Wesley (2003)

Published by: Addison-Wesley (2003)

ISBN: 0-321-15078-3

ISBN: 0-321-15078-3

By: Mary Poppendieck and Tom Poppendieck

By: Mary Poppendieck and Tom Poppendieck

This excellent book describes 22 "thinking tools" that are used to work quickly and effectively in many domains. The authors describe how to apply these tools to software development. If you want to understand why agile development methods work, this book is a must read!

This excellent book describes 22 "thinking tools" that are used to work quickly and effectively in many domains. The authors describe how to apply these tools to software development. If you want to understand why agile development methods work, this book is a must read!

 

[MAS]

[MAS]

Mocks Aren't Stubs

Mocks Aren't Stubs

http://www.martinfowler.com/articles/mocksArentStubs.html

http://www.martinfowler.com/articles/mocksArentStubs.html

By: Martin Fowler

By: Martin Fowler

This article clarifies the difference between Mock Objects (page 544) and Test Stubs (page 529). It goes on to describe the two fundamentally different approaches to test-driven development engendered by these differences: "classical TDD" versus "mockist TDD."

This article clarifies the difference between Mock Objects (page 544) and Test Stubs (page 529). It goes on to describe the two fundamentally different approaches to test-driven development engendered by these differences: "classical TDD" versus "mockist TDD."

 

[MRNO]

[MRNO]

Mock Roles, Not Objects

Mock Roles, Not Objects

Paper presented at OOPSLA 2004 in Vancouver, British Columbia, Canada.

Paper presented at OOPSLA 2004 in Vancouver, British Columbia, Canada.

By: Steve Freeman, Tim Mackinnon, Nat Pryce, and Joe Walnes

By: Steve Freeman, Tim Mackinnon, Nat Pryce, and Joe Walnes

This paper describes the use of Mock Objects (page 544) to help the developer discover the signatures of the objects on which the class being designed and tested depends. This approach allows the design of the supporting classes to be deferred until after the client classes have been coded and tested. Members can obtain this paper at the ACM portal http://portal.acm.org/ft_gateway.cfm?id=1028765&type=pdf; nonmembers of the ACM can find it at http://joe.truemesh.com/MockRoles.pdf.

This paper describes the use of Mock Objects (page 544) to help the developer discover the signatures of the objects on which the class being designed and tested depends. This approach allows the design of the supporting classes to be deferred until after the client classes have been coded and tested. Members can obtain this paper at the ACM portal http://portal.acm.org/ft_gateway.cfm?id=1028765&type=pdf; nonmembers of the ACM can find it at http://joe.truemesh.com/MockRoles.pdf.

 

[PEAA]

[PEAA]

Patterns of Enterprise Application Architecture

Patterns of Enterprise Application Architecture

Published by: Addison-Wesley (2003)

Published by: Addison-Wesley (2003)

ISBN: 0-321-12742-0

ISBN: 0-321-12742-0

By: Martin Fowler

By: Martin Fowler

This book is an indispensable handbook of architectural patterns that are applicable to any enterprise application platform. It is a great way to understand how the various approaches to developing large business systems differ.

This book is an indispensable handbook of architectural patterns that are applicable to any enterprise application platform. It is a great way to understand how the various approaches to developing large business systems differ.

 

[PiJV1]

[PiJV1]

Patterns in Java, Volume 1: A Catalog of Reusable Design Patterns Illustrated with UML

Patterns in Java, Volume 1: A Catalog of Reusable Design Patterns Illustrated with UML

Published by: Wiley Publishing (2002)

Published by: Wiley Publishing (2002)

ISBN: 0-471-22729-3

ISBN: 0-471-22729-3

By: Mark Grand

By: Mark Grand

A catalog of design patterns commonly used in Java.

A catalog of design patterns commonly used in Java.

 

Further Reading

Further Reading

http://www.markgrand.com/id1.html

http://www.markgrand.com/id1.html

[PLoPD3]

[PLoPD3]

Pattern Languages of Program Design 3

Pattern Languages of Program Design 3

Published by: Addison-Wesley (1998)

Published by: Addison-Wesley (1998)

ISBN: 0-201-31011-2

ISBN: 0-201-31011-2

Edited by: Robert C. Martin, Dirk Riehle, and Frank Buschmann

Edited by: Robert C. Martin, Dirk Riehle, and Frank Buschmann

A collection of patterns originally workshopped at the Pattern Languages of Programs (PLoP) conferences.

A collection of patterns originally workshopped at the Pattern Languages of Programs (PLoP) conferences.

 

[POSA2]

[POSA2]

Pattern-Oriented Software Architecture, Volume 2: Patterns for Concurrent and Networked Objects

Pattern-Oriented Software Architecture, Volume 2: Patterns for Concurrent and Networked Objects

Published by: Wiley & Sons (2000)

Published by: Wiley & Sons (2000)

ISBN: 0-471-60695-2

ISBN: 0-471-60695-2

By: Douglas Schmidt, Michael Stal, Hans Robert, and Frank Buschmann

By: Douglas Schmidt, Michael Stal, Hans Robert, and Frank Buschmann

This book is the second volume in the highly acclaimed Pattern-Oriented Software Architecture (POSA) series. POSA1 was published in 1996; hence this book is referred to as POSA2. It presents 17 interrelated patterns that cover core elements of building concurrent and networked systems: service access and configuration, event handling, synchronization, and concurrency.

This book is the second volume in the highly acclaimed Pattern-Oriented Software Architecture (POSA) series. POSA1 was published in 1996; hence this book is referred to as POSA2. It presents 17 interrelated patterns that cover core elements of building concurrent and networked systems: service access and configuration, event handling, synchronization, and concurrency.

 

[PUT]

[PUT]

Pragmatic Unit Testing

Pragmatic Unit Testing

Published by: Pragmatic Bookshelf

Published by: Pragmatic Bookshelf

ISBN: 0-9745140-2-0 (In C# with NUnit)

ISBN: 0-9745140-2-0 (In C# with NUnit)

ISBN: 0-9745140-1-2 (In Java with JUnit)

ISBN: 0-9745140-1-2 (In Java with JUnit)

By: Andy Hunt and Dave Thomas

By: Andy Hunt and Dave Thomas

This book by the "pragmatic programmers" introduces the concept of automated unit testing in a very approachable way. Both versions lower the entry barriers by focusing on the essentials without belaboring the finer points. They also include a very good section on how to determine which tests you need to write for a particular class or method.

This book by the "pragmatic programmers" introduces the concept of automated unit testing in a very approachable way. Both versions lower the entry barriers by focusing on the essentials without belaboring the finer points. They also include a very good section on how to determine which tests you need to write for a particular class or method.

 

[RDb]

[RDb]

Refactoring Databases: Evolutionary Database Design

Refactoring Databases: Evolutionary Database Design

Published by: Addison-Wesley (2006)

Published by: Addison-Wesley (2006)

ISBN: 0-321-29353-3

ISBN: 0-321-29353-3

By: Pramodkumar J. Sadalage and Scott W. Ambler

By: Pramodkumar J. Sadalage and Scott W. Ambler

This book is a good introduction to techniques for applying agile principles to development of database-dependent software. It describes techniques for eliminating the need to do "big design up front" on the database. It deserves to be on the bookshelf of every agile developer who needs to work with a database. A summary of the contents can be found at http://www.ambysoft.com/books/refactoringDatabases.html.

This book is a good introduction to techniques for applying agile principles to development of database-dependent software. It describes techniques for eliminating the need to do "big design up front" on the database. It deserves to be on the bookshelf of every agile developer who needs to work with a database. A summary of the contents can be found at http://www.ambysoft.com/books/refactoringDatabases.html.

 

[Ref]

[Ref]

Refactoring: Improving the Design of Existing Code

Refactoring: Improving the Design of Existing Code

Published by: Addison-Wesley (1999)

Published by: Addison-Wesley (1999)

ISBN: 0-201-48567-2

ISBN: 0-201-48567-2

By: Martin Fowler et al.

By: Martin Fowler et al.

This book offers a good introduction to the process of refactoring software. It introduces a number of "code smells" and suggests ways to refactor the code to eliminate those smells.

This book offers a good introduction to the process of refactoring software. It introduces a number of "code smells" and suggests ways to refactor the code to eliminate those smells.

 

[RTC]

[RTC]

Refactoring Test Code

Refactoring Test Code

Paper presented at XP2001 in Sardinia, Italy

Paper presented at XP2001 in Sardinia, Italy

By: Arie van Deursen, Leon Moonen, Alex van den Bergh, and Gerard Kok

By: Arie van Deursen, Leon Moonen, Alex van den Bergh, and Gerard Kok

This paper was the first to apply the concept of "code smells" to test code. It described a collection of 12 "test smells" and proposed a set of refactorings that could be used to improve the code. The original paper can be found at http://homepages.cwi.nl/~leon/papers/xp2001/xp2001.pdf.

This paper was the first to apply the concept of "code smells" to test code. It described a collection of 12 "test smells" and proposed a set of refactorings that could be used to improve the code. The original paper can be found at http://homepages.cwi.nl/~leon/papers/xp2001/xp2001.pdf.

 

[RtP]

[RtP]

Refactoring to Patterns

Refactoring to Patterns

Published by: Addison-Wesley (2005)

Published by: Addison-Wesley (2005)

ISBN: 0-321-21335-1

ISBN: 0-321-21335-1

By: Joshua Kerievsky

By: Joshua Kerievsky

This book deals with the marriage of refactoring (the process of improving the design of existing code) with patterns (the classic solutions to recurring design problems). Refactoring to Patterns suggests that using patterns to improve an existing design is a better approach than using patterns early in a new design, whether the code is years old or minutes old. We can improve designs with patterns by applying sequences of low-level design transformations, known as refactorings.

This book deals with the marriage of refactoring (the process of improving the design of existing code) with patterns (the classic solutions to recurring design problems). Refactoring to Patterns suggests that using patterns to improve an existing design is a better approach than using patterns early in a new design, whether the code is years old or minutes old. We can improve designs with patterns by applying sequences of low-level design transformations, known as refactorings.

 

[SBPP]

[SBPP]

Smalltalk Best Practice Patterns

Smalltalk Best Practice Patterns

Published by: Prentice Hall (1997)

Published by: Prentice Hall (1997)

ISBN: 0-13-476904-X

ISBN: 0-13-476904-X

By: Kent Beck

By: Kent Beck

This book describes low-level programming patterns that are used in good object-oriented software. On the back cover, Martin Fowler wrote:

This book describes low-level programming patterns that are used in good object-oriented software. On the back cover, Martin Fowler wrote:

 

Kent's Smalltalk style is the standard I aim to emulate in my work. This book does not just set that standard, but also explains why it is the standard. Every Smalltalk developer should have it close at hand.

Kent's Smalltalk style is the standard I aim to emulate in my work. This book does not just set that standard, but also explains why it is the standard. Every Smalltalk developer should have it close at hand.

 

While Smalltalk is no longer the dominant object-oriented development language, many of the patterns established by Smalltalk programmers have been adopted as the standard way of doing things in the mainstream object-oriented development languages. The patterns in this book remain highly relevant even if the examples are in Smalltalk.

While Smalltalk is no longer the dominant object-oriented development language, many of the patterns established by Smalltalk programmers have been adopted as the standard way of doing things in the mainstream object-oriented development languages. The patterns in this book remain highly relevant even if the examples are in Smalltalk.

 

[SCMP]

[SCMP]

Software Configuration Management Patterns: Effective Teamwork, Practical Integration

Software Configuration Management Patterns: Effective Teamwork, Practical Integration

Published by: Addison-Wesley (2003)

Published by: Addison-Wesley (2003)

ISBN: 0-201-74117-1

ISBN: 0-201-74117-1

By: Steve Berczuk (with Brad Appleton)

By: Steve Berczuk (with Brad Appleton)

This book describes, in pattern form, the how's and why's of using a source code configuration management system to synchronize the activities of multiple developers on a project. The practices described here are equally applicable to agile and traditional projects.

This book describes, in pattern form, the how's and why's of using a source code configuration management system to synchronize the activities of multiple developers on a project. The practices described here are equally applicable to agile and traditional projects.

 

Further Reading

Further Reading

http://www.scmpatterns.com

http://www.scmpatterns.com

http://www.scmpatterns.com/book/pattern-summary.html

http://www.scmpatterns.com/book/pattern-summary.html

[SoC]

[SoC]

Secrets of Consulting: A Guide to Giving and Getting Advice Successfully

Secrets of Consulting: A Guide to Giving and Getting Advice Successfully

Published by: Dorset House (1985)

Published by: Dorset House (1985)

ISBN: 0-932633-01-3

ISBN: 0-932633-01-3

By: Gerald M. Weinberg

By: Gerald M. Weinberg

Full of Gerry's laws and rules, such as "The Law of Raspberry Jam: The farther you spread it, the thinner it gets."

Full of Gerry's laws and rules, such as "The Law of Raspberry Jam: The farther you spread it, the thinner it gets."

 

[TAM]

[TAM]

Test Automation Manifesto

Test Automation Manifesto

http://TestAutomationManifesto.gerardmeszaros.com

http://TestAutomationManifesto.gerardmeszaros.com

By: Shaun Smith and Gerard Meszaros

By: Shaun Smith and Gerard Meszaros

This paper was presented at the August 2003 XP/Agile Universe meeting in New Orleans, Louisiana. It describes a number of principles that should be followed to make automated testing using xUnit cost-effective.

This paper was presented at the August 2003 XP/Agile Universe meeting in New Orleans, Louisiana. It describes a number of principles that should be followed to make automated testing using xUnit cost-effective.

 

[TDD-APG]

[TDD-APG]

Test-Driven Development: A Practical Guide

Test-Driven Development: A Practical Guide

Published by: Prentice Hall (2004)

Published by: Prentice Hall (2004)

ISBN: 0-13-101649-0

ISBN: 0-13-101649-0

By: David Astels

By: David Astels

This book provides a good introduction to the process of driving software development with unit tests. Part III of the book is an end-to-end example of using tests to drive a small Java project.

This book provides a good introduction to the process of driving software development with unit tests. Part III of the book is an end-to-end example of using tests to drive a small Java project.

 

[TDD-BE]

[TDD-BE]

Test-Driven Development: By Example

Test-Driven Development: By Example

Published by: Addison-Wesley (2003)

Published by: Addison-Wesley (2003)

ISBN: 0-321-14653-0

ISBN: 0-321-14653-0

By: Kent Beck

By: Kent Beck

This book provides a good introduction to the process of driving software development with unit tests. In the second part of the book, Kent illustrates TDD by building a Test Automation Framework (page 298) in Python. In an approach he likens to "doing brain surgery on yourself," he uses the emerging framework to run the tests he writes for each new capability. It is a very good example of both TDD and bootstrapping.

This book provides a good introduction to the process of driving software development with unit tests. In the second part of the book, Kent illustrates TDD by building a Test Automation Framework (page 298) in Python. In an approach he likens to "doing brain surgery on yourself," he uses the emerging framework to run the tests he writes for each new capability. It is a very good example of both TDD and bootstrapping.

 

[TDD.Net]

[TDD.Net]

Test-Driven Development in Microsoft .NET

Test-Driven Development in Microsoft .NET

Published by: Microsoft Press (2004)

Published by: Microsoft Press (2004)

ISBN: 0-735-61948-4

ISBN: 0-735-61948-4

By: James W. Newkirk and Alexei A. Vorontsov

By: James W. Newkirk and Alexei A. Vorontsov

This book is a good introduction to the test-driven development process and the tools used to do it in Microsoft's. Net development environment.

This book is a good introduction to the test-driven development process and the tools used to do it in Microsoft's. Net development environment.

 

[TI]

[TI]

Test Infected

Test Infected

http://junit.sourceforge.net/doc/testinfected/testing.htm

http://junit.sourceforge.net/doc/testinfected/testing.htm

By: Eric Gamma and Kent Beck

By: Eric Gamma and Kent Beck

This article was first published in the Java Report issue called "Test Infected—Programmers Love Writing Tests." It has been credited by some as being what led to the meteoric rise in JUnit's popularity. This article is an excellent introduction to the how's and why's of test automation using xUnit.

This article was first published in the Java Report issue called "Test Infected—Programmers Love Writing Tests." It has been credited by some as being what led to the meteoric rise in JUnit's popularity. This article is an excellent introduction to the how's and why's of test automation using xUnit.

 

[TPS]

[TPS]

Toyota Production System: Beyond Large-Scale Production

Toyota Production System: Beyond Large-Scale Production

Published by: Productivity Press (1995)

Published by: Productivity Press (1995)

ISBN: 0-915-2991-4-3

ISBN: 0-915-2991-4-3

By: Taiichi Ohno

By: Taiichi Ohno

This book, which was written by the father of just-in-time manufacturing, describes how Toyota came up with the system driven by its need to produce a small number of cars while realizing economies of scale. Among the techniques described here are "kanban" and the "five why's."

This book, which was written by the father of just-in-time manufacturing, describes how Toyota came up with the system driven by its need to produce a small number of cars while realizing economies of scale. Among the techniques described here are "kanban" and the "five why's."

 

[UTF]

[UTF]

Unit Test Frameworks: Tools for High-Quality Software Development

Unit Test Frameworks: Tools for High-Quality Software Development

Published by: O'Reilly (2004)

Published by: O'Reilly (2004)

ISBN: 0-596-00689-6

ISBN: 0-596-00689-6

By: Paul Hamill

By: Paul Hamill

This book is a brief introduction to the most popular implementations of xUnit.

This book is a brief introduction to the most popular implementations of xUnit.

 

[UTwHCM]

[UTwHCM]

Unit Testing with Hand-Crafted Mocks

Unit Testing with Hand-Crafted Mocks

http://refactoring.be/articles/mocks/mocks.html

http://refactoring.be/articles/mocks/mocks.html

By: Sven Gorts

By: Sven Gorts

This paper summarizes and names a number of idioms related to Hand-Built Test Doubles (see Configurable Test Double on page 522)—specifically, Test Stubs (page 529) and Mock Objects (page 544). Sven Gorts writes:

This paper summarizes and names a number of idioms related to Hand-Built Test Doubles (see Configurable Test Double on page 522)—specifically, Test Stubs (page 529) and Mock Objects (page 544). Sven Gorts writes:

 

Many of the unit tests I wrote over the last couple of years use mock objects in order to test the behavior of a component in isolation of the rest of the system. So far, despite the availability of various mocking frameworks, each of the mock classes I've used has been handwritten. In this article I do some retrospection and try to wrap up the mocking idioms I've found most useful.

Many of the unit tests I wrote over the last couple of years use mock objects in order to test the behavior of a component in isolation of the rest of the system. So far, despite the availability of various mocking frameworks, each of the mock classes I've used has been handwritten. In this article I do some retrospection and try to wrap up the mocking idioms I've found most useful.

 

[UTwJ]

[UTwJ]

Unit Testing in Java: How Tests Drive the Code

Unit Testing in Java: How Tests Drive the Code

Published by: Morgan Kaufmann

Published by: Morgan Kaufmann

ISBN: 1-55860-868-0

ISBN: 1-55860-868-0

By: Johannes Link, with contributions by Peter Fröhlich

By: Johannes Link, with contributions by Peter Fröhlich

This book does a very nice job of introducing many of the concepts and techniques of unit testing. It uses intertwined narratives and examples to introduce a wide range of techniques. Unfortunately, due to the format, it can be difficult to find something at a later time.

This book does a very nice job of introducing many of the concepts and techniques of unit testing. It uses intertwined narratives and examples to introduce a wide range of techniques. Unfortunately, due to the format, it can be difficult to find something at a later time.

 

[VCTP]

[VCTP]

The Virtual Clock Test Pattern

The Virtual Clock Test Pattern

http://www.nusco.org/docs/virtual_clock.pdf

http://www.nusco.org/docs/virtual_clock.pdf

By: Paolo Perrotta

By: Paolo Perrotta

This paper describes a common example of a Responder called Virtual Clock [VCTP]. The author uses the Virtual Clock Test Pattern as a Decorator [GOF] for the real system clock, which allows the time to be "frozen" or resumed. One could use a Hard-Coded Test Stub or a Configurable Test Stub just as easily for most tests. Paolo Perrotta summarizes the thrust of his article:

This paper describes a common example of a Responder called Virtual Clock [VCTP]. The author uses the Virtual Clock Test Pattern as a Decorator [GOF] for the real system clock, which allows the time to be "frozen" or resumed. One could use a Hard-Coded Test Stub or a Configurable Test Stub just as easily for most tests. Paolo Perrotta summarizes the thrust of his article:

 

We can have a hard time unit-testing code that depends on the system clock. This paper describes both the problem and a common, reusable solution.

We can have a hard time unit-testing code that depends on the system clock. This paper describes both the problem and a common, reusable solution.

 

[WEwLC]

[WEwLC]

Working Effectively with Legacy Code

Working Effectively with Legacy Code

Published by: Prentice Hall (2005)

Published by: Prentice Hall (2005)

ISBN: 0-13-117705-2

ISBN: 0-13-117705-2

By: Michael Feathers

By: Michael Feathers

This book describes how to get your legacy software system back under control by retrofitting automated unit tests. A key contribution is a set of "dependency-breaking techniques"—mostly refactorings—that can help you isolate the software for the purpose of automated testing.

This book describes how to get your legacy software system back under control by retrofitting automated unit tests. A key contribution is a set of "dependency-breaking techniques"—mostly refactorings—that can help you isolate the software for the purpose of automated testing.

 

[Wp]

[Wp]

Wikipedia

Wikipedia

From Wikipedia [Wp]: "Wikipedia is a multilingual, Web-based free content encyclopedia project. The name Wikipedia is a blend of the words 'wiki' and 'encyclopedia.' Wikipedia is written collaboratively by volunteers, allowing most articles to be changed by almost anyone with access to the Web site."

From Wikipedia [Wp]: "Wikipedia is a multilingual, Web-based free content encyclopedia project. The name Wikipedia is a blend of the words 'wiki' and 'encyclopedia.' Wikipedia is written collaboratively by volunteers, allowing most articles to be changed by almost anyone with access to the Web site."

 

[WWW]

[WWW]

World Wide Web

World Wide Web

A reference annotation of [WWW] indicates that the information was found on the World Wide Web. You can use your favorite search engine to find a copy by searching for it by the title.

A reference annotation of [WWW] indicates that the information was found on the World Wide Web. You can use your favorite search engine to find a copy by searching for it by the title.

 

[XP123]

[XP123]

XP123

XP123

http://xp123.com

http://xp123.com

Web site hosted by: William Wake

Web site hosted by: William Wake

A Web site hosting various resources for teams doing eXtreme Programming.

A Web site hosting various resources for teams doing eXtreme Programming.

 

[XPC]

[XPC]

XProgramming.com

XProgramming.com

http://xprogramming.com

http://xprogramming.com

Web site hosted by: Ron Jeffries

Web site hosted by: Ron Jeffries

A Web site hosting various resources for teams doing eXtreme Programming. One of the better places to look for links to software downloads for unit test automation tools including members of the xUnit family.

A Web site hosting various resources for teams doing eXtreme Programming. One of the better places to look for links to software downloads for unit test automation tools including members of the xUnit family.

 

[XPE]

[XPE]

eXtreme Programming Explained, Second Edition: Embrace Change

eXtreme Programming Explained, Second Edition: Embrace Change

Published by: Addison-Wesley (2005)

Published by: Addison-Wesley (2005)

ISBN: 0-321-27865-8

ISBN: 0-321-27865-8

By: Kent Beck

By: Kent Beck

This book kick-started the eXtreme Programming movement. The first edition (0-201-61641-6) described a recipe consisting of 12 practices backed by principles and values. The second edition focuses more on the values and principles. It breaks the practices into a primary set and a corollary set; the latter set should be attempted only after the primary practices are mastered. Among the practices both editions describe are pair programming and test-driven development.

This book kick-started the eXtreme Programming movement. The first edition (0-201-61641-6) described a recipe consisting of 12 practices backed by principles and values. The second edition focuses more on the values and principles. It breaks the practices into a primary set and a corollary set; the latter set should be attempted only after the primary practices are mastered. Among the practices both editions describe are pair programming and test-driven development.

 

Index

Index

 

A

A

ABAP Object Unit

ABAP Object Unit

ABAP Unit

ABAP Unit

Abstract Setup Decorator

Abstract Setup Decorator

defined

defined

example

example

acceptance tests. See also customer tests

acceptance tests. See also customer tests

defined

defined

why test?

why test?

accessor methods

accessor methods

ACID

ACID

acknowledgements

acknowledgements

action components

action components

agile method

agile method

defined

defined

property tests

property tests

AllTests Suite

AllTests Suite

example

example

introduction

introduction

when to use

when to use

annotation

annotation

defined

defined

Test Methods

Test Methods

Anonymous Creation Method

Anonymous Creation Method

defined

defined

example

example

Hard-Coded Test Data solution

Hard-Coded Test Data solution

preface

preface

anonymous inner class

anonymous inner class

defined

defined

Test Stub examples

Test Stub examples

Ant

Ant

AntHill

AntHill

anti-pattern (AP)

anti-pattern (AP)

defined

defined

test smells

test smells

AOP (aspect-oriented programming)

AOP (aspect-oriented programming)

defined

defined

Dependency Injection

Dependency Injection

retrofitting testability

retrofitting testability

API (application programming interface)

API (application programming interface)

Creation Methods

Creation Methods

database as SUT

database as SUT

defined

defined

Test Utility Method

Test Utility Method

architecture, design for testability. See design-for-testability

architecture, design for testability. See design-for-testability

arguments

arguments

messages describing

messages describing

as parameters (Dummy Arguments)

as parameters (Dummy Arguments)

role-describing

role-describing

Arguments, Dummy

Arguments, Dummy

Ariane 5 rocket

Ariane 5 rocket

aspect-oriented programming (AOP)

aspect-oriented programming (AOP)

defined

defined

Dependency Injection

Dependency Injection

retrofitting testability

retrofitting testability

Assertion Message

Assertion Message

of Assertion Method

of Assertion Method

pattern description

pattern description

Assertion Method

Assertion Method

Assertion Messages

Assertion Messages

calling built-in

calling built-in

choosing right

choosing right

Equality Assertions

Equality Assertions

examples

examples

Expected Exception Assertions

Expected Exception Assertions

Fuzzy Equality Assertions

Fuzzy Equality Assertions

implementation

implementation

as macros

as macros

motivating example

motivating example

overview

overview

refactoring

refactoring

Single-Outcome Assertions

Single-Outcome Assertions

Stated Outcome Assertions

Stated Outcome Assertions

Assertion Roulette

Assertion Roulette

Eager Tests

Eager Tests

impact

impact

introduction

introduction

Missing Assertion Message

Missing Assertion Message

symptoms

symptoms

assertions

assertions

Built-in

Built-in

custom. See Custom Assertion

custom. See Custom Assertion

defined

defined

diagramming notation

diagramming notation

Domain Assertions

Domain Assertions

improperly coded in Neverfail Tests

improperly coded in Neverfail Tests

introduction

introduction

Missing Assertion Messages

Missing Assertion Messages

reducing Test Code Duplication

reducing Test Code Duplication

refactoring

refactoring

Self-Checking Tests

Self-Checking Tests

unit testing

unit testing

Verify One Condition per Test

Verify One Condition per Test

assumptions

assumptions

Astels, Dave

Astels, Dave

asynchronous tests

asynchronous tests

defined

defined

Hard-To-Test Code

Hard-To-Test Code

Humble Object

Humble Object

Slow Tests

Slow Tests

testability

testability

Attachment Method

Attachment Method

defined

defined

example

example

attributes

attributes

defined

defined

dummy

dummy

hiding unnecessary

hiding unnecessary

One Bad Attribute. See One Bad Attribute

One Bad Attribute. See One Bad Attribute

parameters as

parameters as

Suite Fixture Setup

Suite Fixture Setup

Test Discovery using

Test Discovery using

Test Selection

Test Selection

Automated Exercise Teardown

Automated Exercise Teardown

defined

defined

example

example

Automated Fixture Teardown

Automated Fixture Teardown

Automated Teardown

Automated Teardown

ensuring Repeatable Tests

ensuring Repeatable Tests

examples

examples

implementation

implementation

Interacting Test Suites

Interacting Test Suites

Interacting Tests solution

Interacting Tests solution

motivating example

motivating example

overview

overview

of persistent fixtures

of persistent fixtures

refactoring

refactoring

resource leakage solution

resource leakage solution

when to use

when to use

automated unit testing

automated unit testing

author's motivation

author's motivation

fragile test problem

fragile test problem

introduction

introduction

B

B

back door, defined

back door, defined

Back Door Manipulation

Back Door Manipulation

control/observation points

control/observation points

database as SUT API

database as SUT API

Expected State Specification

Expected State Specification

fixture setup

fixture setup

implementation

implementation

motivating example

motivating example

overview

overview

refactoring

refactoring

setup

setup

teardown

teardown

verification

verification

verification using Test Spy

verification using Test Spy

when to use

when to use

Back Door Setup

Back Door Setup

controlling indirect inputs

controlling indirect inputs

fixture design

fixture design

Prebuilt Fixtures

Prebuilt Fixtures

transient fixtures

transient fixtures

Back Door Verification

Back Door Verification

BDUF (big design upfront)

BDUF (big design upfront)

defined

defined

design for testability

design for testability

test automation strategy

test automation strategy

Beck, Kent

Beck, Kent

sniff test

sniff test

Test Automation Frameworks

Test Automation Frameworks

test smells

test smells

Testcase Class per Class

Testcase Class per Class

xUnit

xUnit

Behavior Sensitivity

Behavior Sensitivity

cause of Fragile Tests

cause of Fragile Tests

caused by Overspecified Software

caused by Overspecified Software

defined

defined

smells

smells

behavior smells

behavior smells

Assertion Roulette. See Assertion Roulette

Assertion Roulette. See Assertion Roulette

defined

defined

Erratic Tests. See Erratic Test

Erratic Tests. See Erratic Test

Fragile Tests. See Fragile Test

Fragile Tests. See Fragile Test

Frequent Debugging. See Frequent Debugging

Frequent Debugging. See Frequent Debugging

Manual Intervention. See Manual Intervention

Manual Intervention. See Manual Intervention

overview

overview

Slow Tests. See Slow Tests

Slow Tests. See Slow Tests

Behavior Verification

Behavior Verification

approach to Self-Checking Tests

approach to Self-Checking Tests

examples

examples

implementation

implementation

indirect outputs

indirect outputs

motivating example

motivating example

overview

overview

refactoring

refactoring

vs. state

vs. state

test results

test results

using Mock Objects. See Mock Object

using Mock Objects. See Mock Object

using Test Spies. See Test Spy

using Test Spies. See Test Spy

using Use the Front Door First

using Use the Front Door First

verifying indirect outputs

verifying indirect outputs

when to use

when to use

behavior-driven development

behavior-driven development

defined

defined

Testcase Class per Fixture usage

Testcase Class per Fixture usage

Behavior-Exposing Subclass

Behavior-Exposing Subclass

Test-Specific Subclass example

Test-Specific Subclass example

when to use

when to use

Behavior-Modifying Subclass

Behavior-Modifying Subclass

Defining Test-Specific Equality

Defining Test-Specific Equality

Substituted Singleton

Substituted Singleton

Test Stub

Test Stub

when to use

when to use

Bespoke Assertion. See Custom Assertion

Bespoke Assertion. See Custom Assertion

bimodal tests

bimodal tests

binding, static

binding, static

defined

defined

Dependency Injection

Dependency Injection

black box

black box

defined

defined

Remoted Stored Procedure Tests

Remoted Stored Procedure Tests

block closures

block closures

defined

defined

Expected Exception Tests

Expected Exception Tests

blocks

blocks

cleaning up fixture teardown logic

cleaning up fixture teardown logic

defined

defined

try/finally. See try/finally block

try/finally. See try/finally block

boundary values

boundary values

defined

defined

erratic tests

erratic tests

Minimal Fixtures

Minimal Fixtures

result verification patterns

result verification patterns

BPT (Business Process Testing)

BPT (Business Process Testing)

defined

defined

Recorded Tests

Recorded Tests

Test Automation Frameworks

Test Automation Frameworks

Bug Repellent

Bug Repellent

Buggy Test

Buggy Test

introduction

introduction

reducing risk

reducing risk

symptoms

symptoms

Built-in Assertion

Built-in Assertion

calling

calling

introduction

introduction

built-in self-tests

built-in self-tests

defined

defined

test file organization

test file organization

built-in test recording

built-in test recording

defined

defined

example

example

business logic

business logic

defined

defined

developer testing

developer testing

development process

development process

Layer Tests example

Layer Tests example

testing without databases

testing without databases

Business Process Testing (BPT). See BPT (Business Process Testing)

Business Process Testing (BPT). See BPT (Business Process Testing)

C

C

Calculated Value. See also Derived Value

Calculated Value. See also Derived Value

Loop-Driven Tests

Loop-Driven Tests

Production Logic in Test solution

Production Logic in Test solution

Canoo WebTest

Canoo WebTest

defined

defined

Scripted Tests

Scripted Tests

Test Automation Frameworks

Test Automation Frameworks

test automation tools

test automation tools

capacity tests

capacity tests

Capture/Playback Test. See Recorded Test

Capture/Playback Test. See Recorded Test

Chained Test

Chained Test

customer testing

customer testing

examples

examples

implementation

implementation

motivating example

motivating example

overview

overview

refactoring

refactoring

Shared Fixture strategies

Shared Fixture strategies

Shared Fixtures

Shared Fixtures

when to use

when to use

xUnit introduction

xUnit introduction

class attributes

class attributes

defined

defined

Test Discovery using

Test Discovery using

Testcase Class Selection using

Testcase Class Selection using

class methods

class methods

defined

defined

with Test Helper

with Test Helper

class variables

class variables

defined

defined

Suite Fixture Setup

Suite Fixture Setup

classes

classes

diagramming notation

diagramming notation

as fixtures

as fixtures

Test Double

Test Double

Testcase. See Testcase Class

Testcase. See Testcase Class

class-instance duality

class-instance duality

Cleanup Method

Cleanup Method

closure, block

closure, block

defined

defined

Expected Exception Tests

Expected Exception Tests

Cockburn, Alistair

Cockburn, Alistair

pattern naming

pattern naming

service layer tests

service layer tests

code

code

inside-out development

inside-out development

organization. See test organization

organization. See test organization

samples

samples

writing tests

writing tests

code smells

code smells

Conditional Test Logic. See Conditional Test Logic

Conditional Test Logic. See Conditional Test Logic

defined

defined

Hard-To-Test Code. See Hard-To-Test Code

Hard-To-Test Code. See Hard-To-Test Code

obscure tests. See Obscure Test

obscure tests. See Obscure Test

Test Code Duplication. See Test Code Duplication

Test Code Duplication. See Test Code Duplication

Test Logic in Production. See Test Logic in Production

Test Logic in Production. See Test Logic in Production

types of

types of

coding idioms

coding idioms

defined

defined

design patterns

design patterns

collisions

collisions

Interacting Tests

Interacting Tests

Shared Fixtures

Shared Fixtures

Command object

Command object

introduction

introduction

Testcase Object as

Testcase Object as

Command-Line Test Runner

Command-Line Test Runner

Assertion Message

Assertion Message

defined

defined

introduction

introduction

Missing Assertion Message

Missing Assertion Message

commercial recorded tests

commercial recorded tests

refactored

refactored

tools

tools

common location, Test Discovery

common location, Test Discovery

Communicate Intent

Communicate Intent

defined

defined

refactoring Recorded Tests to

refactoring Recorded Tests to

compiler macro, Test Method Discovery

compiler macro, Test Method Discovery

Complex Teardown

Complex Teardown

Complex Test. See Dependency Lookup

Complex Test. See Dependency Lookup

Component Broker. See Dependency Lookup

Component Broker. See Dependency Lookup

Component Registry

Component Registry

component tests

component tests

defined

defined

layer-crossing tests

layer-crossing tests

per-functionality

per-functionality

test automation philosophies

test automation philosophies

test strategy patterns

test strategy patterns

components

components

defined

defined

depended-on component. See DOC (depended-on component)

depended-on component. See DOC (depended-on component)

Composite object, defined

Composite object, defined

Concerns, Separation of

Concerns, Separation of

concrete classes

concrete classes

Condition Verification Logic

Condition Verification Logic

Conditional Test Logic

Conditional Test Logic

vs. Assertion Method

vs. Assertion Method

avoidance

avoidance

avoiding via Custom Assertion

avoiding via Custom Assertion

avoiding via Guard Assertion

avoiding via Guard Assertion

causes

causes

Complex Teardown

Complex Teardown

Condition Verification Logic

Condition Verification Logic

Flexible Tests

Flexible Tests

impact

impact

introduction

introduction

Multiple Test Conditions

Multiple Test Conditions

Production Logic in Test

Production Logic in Test

symptoms

symptoms

Test Methods

Test Methods

Configurable Mock Object. See also Configurable Test Double

Configurable Mock Object. See also Configurable Test Double

Configurable Registry

Configurable Registry

Configurable Test Double

Configurable Test Double

examples

examples

implementation

implementation

installing

installing

as kind of Test Double

as kind of Test Double

motivating example

motivating example

overview

overview

refactoring

refactoring

when to use

when to use

Configurable Test Stub. See also Configurable Test Double

Configurable Test Stub. See also Configurable Test Double

implementation

implementation

indirect input control

indirect input control

Configuration Interface

Configuration Interface

examples

examples

implementation

implementation

Configuration Mode

Configuration Mode

example

example

implementation

implementation

Constant Value. See Literal Value

Constant Value. See Literal Value

constants in Derived Value

constants in Derived Value

constructing Mock Object

constructing Mock Object

Constructor Injection

Constructor Injection

example

example

implementation

implementation

installing Test Doubles

installing Test Doubles

Constructor Test

Constructor Test

defined

defined

example

example

introduction

introduction

constructors

constructors

defined

defined

problems with

problems with

containers, Humble Container Adapter

containers, Humble Container Adapter

Context Sensitivity

Context Sensitivity

avoiding via Isolate the SUT

avoiding via Isolate the SUT

defined

defined

introduction

introduction

continuous design

continuous design

continuous integration

continuous integration

avoiding Lost Tests

avoiding Lost Tests

defined

defined

impact of Data-Driven Tests

impact of Data-Driven Tests

steps

steps

control points

control points

defined

defined

testability

testability

Coplien, Jim

Coplien, Jim

CORBA standards

CORBA standards

cost effectiveness, Self-Checking Tests

cost effectiveness, Self-Checking Tests

costs, test automation

costs, test automation

Covey, Stephen

Covey, Stephen

CppUnit

CppUnit

defined

defined

Test Automation Frameworks

Test Automation Frameworks

Test Method enumeration

Test Method enumeration

Creation Method

Creation Method

Delegated Setup

Delegated Setup

eliminating unnecessary objects/attributes

eliminating unnecessary objects/attributes

examples

examples

as Hard-Coded Test Data solution

as Hard-Coded Test Data solution

hybrid setup

hybrid setup

implementation

implementation

motivating example

motivating example

overview

overview

persistent fixtures teardown

persistent fixtures teardown

preface

preface

refactoring

refactoring

as Test Utility Method

as Test Utility Method

when to use

when to use

writing simple tests

writing simple tests

cross-functional tests

cross-functional tests

cross-thread failure assertion

cross-thread failure assertion

Cruise Control

Cruise Control

CsUnit

CsUnit

CSV files, xUnit Data-Driven Test

CSV files, xUnit Data-Driven Test

CUnit

CUnit

Cunningham, Ward

Cunningham, Ward

Custom Assertion

Custom Assertion

as Conditional Verification Logic solution

as Conditional Verification Logic solution

examples

examples

implementation

implementation

Indirect Testing solution

Indirect Testing solution

Irrelevant Information solution

Irrelevant Information solution

motivating example

motivating example

overview

overview

reducing Test Code Duplication

reducing Test Code Duplication

refactoring

refactoring

Test Utility Methods

Test Utility Methods

when to use

when to use

writing simple tests

writing simple tests

Custom Assertion test

Custom Assertion test

example

example

implementation

implementation

Custom Equality Assertion

Custom Equality Assertion

customer tests

customer tests

defined

defined

Eager Tests cause

Eager Tests cause

Missing Unit Test

Missing Unit Test

overview

overview

per-functionality

per-functionality

as Scripted Test

as Scripted Test

Cut and Paste code reuse

Cut and Paste code reuse

D

D

data access layer

data access layer

database testing

database testing

defined

defined

Slow Tests with Shared Fixtures

Slow Tests with Shared Fixtures

data leaks

data leaks

avoiding with Delta Assertions

avoiding with Delta Assertions

Complex Teardown

Complex Teardown

Data Loader, Back Door Manipulation

Data Loader, Back Door Manipulation

data minimization

data minimization

data population script

data population script

Data Retriever

Data Retriever

Data Sensitivity

Data Sensitivity

defined

defined

introduction

introduction

Data Transfer Object (DTO)

Data Transfer Object (DTO)

defined

defined

result verification

result verification

Database Extraction Script

Database Extraction Script

Database Partitioning Scheme

Database Partitioning Scheme

Data Sensitivity solution

Data Sensitivity solution

developer independence

developer independence

example

example

Global Fixtures

Global Fixtures

implementation

implementation

database patterns

database patterns

Database Sandbox

Database Sandbox

Stored Procedure Test

Stored Procedure Test

Table Truncation Teardown

Table Truncation Teardown

Transaction Rollback Teardown

Transaction Rollback Teardown

Database Population Script

Database Population Script

Database Sandbox

Database Sandbox

database testing

database testing

design for testability

design for testability

pattern description

pattern description

as Test Run Wars solution

as Test Run Wars solution

Unrepeatable Tests cause

Unrepeatable Tests cause

when to use

when to use

database testing

database testing

overview

overview

persistent fixtures

persistent fixtures

testing without databases

testing without databases

types of

types of

Database Transaction Rollback Teardown

Database Transaction Rollback Teardown

databases

databases

fake. See Fake Database

fake. See Fake Database

as SUT API

as SUT API

teardown

teardown

Data-Driven Test

Data-Driven Test

customer testing

customer testing

Fit framework example

Fit framework example

frameworks

frameworks

implementation

implementation

implemented as Recorded Test

implemented as Recorded Test

introduction

introduction

motivating example

motivating example

overview

overview

principles

principles

reducing Test Code Duplication

reducing Test Code Duplication

refactoring notes

refactoring notes

Test Suite Object Simulator

Test Suite Object Simulator

using Fit framework

using Fit framework

via Naive xUnit Test Interpreter

via Naive xUnit Test Interpreter

via Test Suite Object Generator

via Test Suite Object Generator

when to use

when to use

xUnit with CSV input file

xUnit with CSV input file

xUnit with XML data file

xUnit with XML data file

DB Schema per Test Runner

DB Schema per Test Runner

developer independence

developer independence

implementation

implementation

DbUnit

DbUnit

Back Door Manipulation

Back Door Manipulation

defined

defined

Expected State Specification

Expected State Specification

DDSteps

DDSteps

Decorated Lazy Setup

Decorated Lazy Setup

Decorator

Decorator

Abstract Setup Decorator

Abstract Setup Decorator

Parameterized Setup Decorator

Parameterized Setup Decorator

Pushdown Decorator

Pushdown Decorator

Setup. See Setup Decorator

Setup. See Setup Decorator

Test Hook as

Test Hook as

Dedicated Database Sandbox

Dedicated Database Sandbox

Defect Localization

Defect Localization

customer testing

customer testing

defined

defined

Frequent Debugging

Frequent Debugging

Keep Tests Independent Tests

Keep Tests Independent Tests

right-sizing Test Methods

right-sizing Test Methods

test automation philosophies

test automation philosophies

unit testing

unit testing

Verify One Condition per Test

Verify One Condition per Test

defining tests

defining tests

introduction

introduction

suites of

suites of

delays. See Slow Tests

delays. See Slow Tests

Delegated Setup

Delegated Setup

example

example

introduction

introduction

matching with teardown code

matching with teardown code

overview

overview

of transient fixtures

of transient fixtures

when to use

when to use

Delegated Teardown

Delegated Teardown

example

example

overview

overview

of persistent fixtures

of persistent fixtures

Table Truncation Teardown

Table Truncation Teardown

Delta Assertion

Delta Assertion

avoiding fixture collisions

avoiding fixture collisions

as Data Sensitivity solution

as Data Sensitivity solution

detecting data leakage with

detecting data leakage with

examples

examples

introduction

introduction

pattern description

pattern description

depended-on component (DOC). See DOC (depended-on component)

depended-on component (DOC). See DOC (depended-on component)

dependencies

dependencies

Interacting Tests

Interacting Tests

replacement with Test Doubles

replacement with Test Doubles

replacing using Test Hooks

replacing using Test Hooks

retrofitting testability

retrofitting testability

test automation philosophies

test automation philosophies

Test Dependency in Production

Test Dependency in Production

test file organization

test file organization

Dependency Initialization Test

Dependency Initialization Test

Dependency Injection

Dependency Injection

design for testability

design for testability

examples

examples

implementation

implementation

installing Test Doubles via

installing Test Doubles via

Isolate the SUT

Isolate the SUT

motivating example

motivating example

overview

overview

Persistent Fresh Fixtures avoidance

Persistent Fresh Fixtures avoidance

refactoring

refactoring

testability improvement

testability improvement

when database testing

when database testing

when to use

when to use

Dependency Lookup

Dependency Lookup

design for testability

design for testability

examples

examples

implementation

implementation

installing Test Doubles

installing Test Doubles

Isolate the SUT

Isolate the SUT

motivating example

motivating example

names

names

overview

overview

Persistent Fresh Fixtures

Persistent Fresh Fixtures

refactoring

refactoring

when database testing

when database testing

when to use

when to use

Derived Expectation

Derived Expectation

example

example

when to use

when to use

Derived Input

Derived Input

Derived Value

Derived Value

examples

examples

overview

overview

when to use

when to use

design patterns

design patterns

design-for-testability

design-for-testability

control points and observation points

control points and observation points

defined

defined

divide and test

divide and test

ensuring testability

ensuring testability

interaction styles and testability patterns

interaction styles and testability patterns

overview

overview

Separation of Concerns

Separation of Concerns

test automation philosophies. See test automation philosophies

test automation philosophies. See test automation philosophies

test automation principles

test automation principles

test-driven testability

test-driven testability

design-for-testability patterns

design-for-testability patterns

Dependency Injection. See Dependency Injection

Dependency Injection. See Dependency Injection

Dependency Lookup. See Dependency Lookup

Dependency Lookup. See Dependency Lookup

Humble Object. See Humble Object

Humble Object. See Humble Object

Test Hooks

Test Hooks

deterministic values

deterministic values

developer independence

developer independence

developer testing

developer testing

defined

defined

introduction

introduction

Developers Not Writing Tests

Developers Not Writing Tests

development

development

agile

agile

behavior driven

behavior driven

document-driven

document-driven

EDD. See EDD (example-driven development)

EDD. See EDD (example-driven development)

incremental

incremental

inside-out

inside-out

inside-out vs. outside in

inside-out vs. outside in

need-driven. See need-driven development

need-driven. See need-driven development

outside-in

outside-in

process

process

TDD. See TDD (test-driven development)

TDD. See TDD (test-driven development)

test-first. See test-first development

test-first. See test-first development

test-last. See test-last development

test-last. See test-last development

Diagnostic Assertion

Diagnostic Assertion

diagramming notation

diagramming notation

Dialog, Humble. See Humble Dialog

Dialog, Humble. See Humble Dialog

direct output

direct output

defined

defined

verification

verification

Direct Test Method Invocation

Direct Test Method Invocation

disambiguation, test fixtures

disambiguation, test fixtures

Discovery, Test. See Test Discovery

Discovery, Test. See Test Discovery

Distinct Generated Values

Distinct Generated Values

Anonymous Creation Methods

Anonymous Creation Methods

Delegated Setup

Delegated Setup

example

example

Hard-Coded Test Data solution

Hard-Coded Test Data solution

implementation

implementation

Unrepeatable Tests solution

Unrepeatable Tests solution

Distinct Values

Distinct Values

Do No Harm

Do No Harm

DOC (depended-on component)

DOC (depended-on component)

Behavior Verification

Behavior Verification

control points and observation points

control points and observation points

defined

defined

outside-in development

outside-in development

replacing with Test Double. See Test Double

replacing with Test Double. See Test Double

retrieving. See Dependency Lookup

retrieving. See Dependency Lookup

terminology

terminology

Test Hook in

Test Hook in

Documentation, Tests as. See Tests as Documentation

Documentation, Tests as. See Tests as Documentation

document-driven development

document-driven development

Domain Assertion

Domain Assertion

defined

defined

example

example

domain layer

domain layer

defined

defined

test strategy patterns

test strategy patterns

domain model

domain model

Don't Modify the SUT

Don't Modify the SUT

drivers, test

drivers, test

defined

defined

lack of Assertion Messages

lack of Assertion Messages

DRY (don't repeat yourself)

DRY (don't repeat yourself)

DTO (Data Transfer Object)

DTO (Data Transfer Object)

defined

defined

result verification

result verification

Dummy Argument

Dummy Argument

Dummy Attribute

Dummy Attribute

Dummy Object

Dummy Object

configuring

configuring

defined

defined

as Test Double

as Test Double

as value pattern

as value pattern

xUnit terminology

xUnit terminology

dynamic binding

dynamic binding

defined

defined

use in Dependency Injection

use in Dependency Injection

Dynamically Generated Mock Object

Dynamically Generated Mock Object

Dynamically Generated Test Double

Dynamically Generated Test Double

implementation

implementation

providing

providing

Dynamically Generated Test Stub

Dynamically Generated Test Stub

E

E

Eager Test

Eager Test

Assertion Roulette

Assertion Roulette

Fragile Tests

Fragile Tests

Obscure Tests

Obscure Tests

right-sizing Test Methods

right-sizing Test Methods

EasyMock

EasyMock

defined

defined

Test Doubles

Test Doubles

eCATT

eCATT

defined

defined

Test Automation Frameworks

Test Automation Frameworks

Eclipse

Eclipse

Debugger

Debugger

defined

defined

economics of test automation

economics of test automation

EDD (example-driven development)

EDD (example-driven development)

defined

defined

tests as examples

tests as examples

efficiency

efficiency

emergent design

emergent design

vs. BDUF

vs. BDUF

defined

defined

encapsulation

encapsulation

Creation Method. See Creation Method

Creation Method. See Creation Method

Dependency Lookup implementation

Dependency Lookup implementation

indirect outputs and

indirect outputs and

Indirect Testing solution

Indirect Testing solution

SUT API. See SUT API Encapsulation

SUT API. See SUT API Encapsulation

using Test Utility Methods. See Test Utility Method

using Test Utility Methods. See Test Utility Method

endoscopic testing (ET)

endoscopic testing (ET)

defined

defined

Mock Objects

Mock Objects

Test Doubles

Test Doubles

Ensure Commensurate Effort and Responsibility

Ensure Commensurate Effort and Responsibility

Entity Chain Snipping

Entity Chain Snipping

example

example

testing with doubles

testing with doubles

when to use

when to use

entity object

entity object

enumeration

enumeration

customer testing

customer testing

Suite of Suites built using

Suite of Suites built using

test conditions in Loop-Driven Tests

test conditions in Loop-Driven Tests

Test Enumeration

Test Enumeration

Test Suite Object built using

Test Suite Object built using

xUnit organization mechanisms

xUnit organization mechanisms

Equality, Sensitivity

Equality, Sensitivity

Fragile Tests

Fragile Tests

test-first development

test-first development

Equality Assertion

Equality Assertion

Assertion Methods

Assertion Methods

Custom

Custom

example

example

Guard Assertion as

Guard Assertion as

introduction

introduction

reducing Test Code Duplication

reducing Test Code Duplication

unit testing

unit testing

Equality Pollution

Equality Pollution

equals method

equals method

Equality Pollution

Equality Pollution

Expected State Specification

Expected State Specification

reducing Test Code Duplication

reducing Test Code Duplication

equivalence class

equivalence class

Behavior Smells

Behavior Smells

defined

defined

Untested Code

Untested Code

Erratic Test

Erratic Test

Automated Teardown and

Automated Teardown and

customer testing

customer testing

database testing

database testing

impact

impact

Interacting Test Suites

Interacting Test Suites

Interacting Tests

Interacting Tests

introduction

introduction

Lonely Tests

Lonely Tests

Nondeterministic Tests

Nondeterministic Tests

Resource Leakage

Resource Leakage

Resource Optimism

Resource Optimism

symptoms

symptoms

Test Run Wars

Test Run Wars

troubleshooting

troubleshooting

Unrepeatable Tests

Unrepeatable Tests

essential but irrelevant fixture setup

essential but irrelevant fixture setup

ET (endoscopic testing)

ET (endoscopic testing)

defined

defined

Mock Object use for

Mock Object use for

example-driven development (EDD)

example-driven development (EDD)

defined

defined

tests as examples

tests as examples

examples, tests as

examples, tests as

exclamation marks

exclamation marks

Executable, Humble. See Humble Executable

Executable, Humble. See Humble Executable

Executable Specification

Executable Specification

execution optimization

execution optimization

exercise SUT

exercise SUT

defined

defined

test phases

test phases

expectations

expectations

defined

defined

Derived Expectations

Derived Expectations

messages describing

messages describing

naming conventions

naming conventions

Expected Behavior Specification

Expected Behavior Specification

defined

defined

example

example

Expected Behavior Verification

Expected Behavior Verification

defined

defined

indirect outputs

indirect outputs

Expected Exception Assertion

Expected Exception Assertion

defined as Assertion Method

defined as Assertion Method

example

example

Expected Exception Test

Expected Exception Test

Conditional Verification Logic solution

Conditional Verification Logic solution

introduction

introduction

as Test Method

as Test Method

using block closure

using block closure

using method attributes

using method attributes

using try/catch

using try/catch

Expected Object

Expected Object

reducing Test Code Duplication

reducing Test Code Duplication

refactoring tests

refactoring tests

State Verifications

State Verifications

unit testing

unit testing

expected outcome

expected outcome

Expected State Specification

Expected State Specification

expected values

expected values

exploratory testing

exploratory testing

cross-functionality

cross-functionality

defined

defined

Scripted Tests

Scripted Tests

Expression Builders

Expression Builders

expressiveness gaps

expressiveness gaps

external resource setup

external resource setup

external result verification

external result verification

external test recording

external test recording

Extract Method

Extract Method

Creation Methods

Creation Methods

Custom Assertions

Custom Assertions

Delegated Setup

Delegated Setup

as Eager Tests solution

as Eager Tests solution

example

example

in persistent fixture teardown

in persistent fixture teardown

refactoring Recorded Tests

refactoring Recorded Tests

Extract Testable Component

Extract Testable Component

eXtreme Programming

eXtreme Programming

defined

defined

projects affected by Slow Tests

projects affected by Slow Tests

eXtreme Programming Explained (Beck)

eXtreme Programming Explained (Beck)

F

F

factories

factories

defined

defined

Factory Method

Factory Method

Object Factories

Object Factories

failed tests

failed tests

due to Unfinished Test Assertions

due to Unfinished Test Assertions

implementation

implementation

"Fail-Pass-Pass"

"Fail-Pass-Pass"

failure messages

failure messages

Assertion Messages

Assertion Messages

Built-in Assertions

Built-in Assertions

removing "if" statements

removing "if" statements

Single-Outcome Assertions

Single-Outcome Assertions

Fake Database

Fake Database

avoiding persistence

avoiding persistence

database testing

database testing

example

example

Slow Component Usage solution

Slow Component Usage solution

Slow Tests with Shared Fixtures

Slow Tests with Shared Fixtures

when to use

when to use

Fake Object

Fake Object

configuring

configuring

customer testing

customer testing

defined

defined

examples

examples

implementation

implementation

motivating example

motivating example

optimizing test execution

optimizing test execution

overview

overview

refactoring

refactoring

as Test Double

as Test Double

when to use

when to use

xUnit terminology

xUnit terminology

Fake Service Layer

Fake Service Layer

Fake Web Services

Fake Web Services

false negative

false negative

false positive

false positive

fault insertion tests

fault insertion tests

defined

defined

per-functionality

per-functionality

Feathers, Michael

Feathers, Michael

Highly Coupled Code solution

Highly Coupled Code solution

Humble Object

Humble Object

pattern naming

pattern naming

retrofitting testability

retrofitting testability

Self Shunt

Self Shunt

test automation roadmap

test automation roadmap

Unit Test Rulz

Unit Test Rulz

features

features

defined

defined

right-sizing Test Methods

right-sizing Test Methods

Testcase Class per. See Testcase Class per Feature

Testcase Class per. See Testcase Class per Feature

visibility/granularity in Test-Specific Subclass

visibility/granularity in Test-Specific Subclass

feedback in test automation

feedback in test automation

file contention. See Test Run War

file contention. See Test Run War

File System Test Runner

File System Test Runner

Finder Method

Finder Method

accessing Shared Fixtures

accessing Shared Fixtures

Mystery Guests solution

Mystery Guests solution

when to use

when to use

fine-grained testing

fine-grained testing

Fit

Fit

Data-Driven Test example

Data-Driven Test example

Data-Driven Test implementation

Data-Driven Test implementation

defined

defined

Expected State Specification

Expected State Specification

fixture definition

fixture definition

fixture vs. Testcase Class

fixture vs. Testcase Class

Scripted Tests implementation

Scripted Tests implementation

Test Automation Framework

Test Automation Framework

test automation tools

test automation tools

tests as examples

tests as examples

vs. xUnit

vs. xUnit

Fitnesse

Fitnesse

Data-Driven Test implementation

Data-Driven Test implementation

defined

defined

Scripted Test implementation

Scripted Test implementation

"Five Whys"

"Five Whys"

fixture design

fixture design

upfront or test-by-test

upfront or test-by-test

Verify One Condition per Test

Verify One Condition per Test

xUnit sweet spot

xUnit sweet spot

fixture holding class variables

fixture holding class variables

fixture holding instance variables

fixture holding instance variables

fixture setup

fixture setup

Back Door Manipulation

Back Door Manipulation

cleaning up

cleaning up

defined

defined

Delegated Setup

Delegated Setup

external resources

external resources

Four-Phase Test

Four-Phase Test

Fresh Fixtures

Fresh Fixtures

hybrid setup

hybrid setup

Implicit Setup

Implicit Setup

In-Line Setup

In-Line Setup

introduction

introduction

matching with teardown code

matching with teardown code

Shared Fixtures

Shared Fixtures

speeding up with doubles

speeding up with doubles

strategies

strategies

fixture setup patterns

fixture setup patterns

Chained Test. See Chained Test

Chained Test. See Chained Test

Creation Method. See Creation Method

Creation Method. See Creation Method

Delegated Setup

Delegated Setup

Implicit Setup. See also Implicit Setup

Implicit Setup. See also Implicit Setup

In-line Setup. See also In-line Setup

In-line Setup. See also In-line Setup

Lazy Setup. See Lazy Setup

Lazy Setup. See Lazy Setup

Prebuilt Fixture. See Prebuilt Fixture

Prebuilt Fixture. See Prebuilt Fixture

Setup Decorator. See Setup Decorator

Setup Decorator. See Setup Decorator

Suite Fixture Setup. See Suite Fixture Setup

Suite Fixture Setup. See Suite Fixture Setup

Fixture Setup Testcase

Fixture Setup Testcase

fixture strategies

fixture strategies

overview

overview

persistent fresh fixtures

persistent fresh fixtures

shared fixture strategies

shared fixture strategies

fixture teardown

fixture teardown

avoiding in persistent fixtures

avoiding in persistent fixtures

Back Door Manipulation

Back Door Manipulation

cleaning up

cleaning up

Complex Teardown

Complex Teardown

data access layer testing

data access layer testing

defined

defined

fixture strategies

fixture strategies

Four-Phase Test

Four-Phase Test

Implicit Setup

Implicit Setup

introduction

introduction

Lazy Setup problems

Lazy Setup problems

persistent fixtures

persistent fixtures

Persistent Fresh Fixtures

Persistent Fresh Fixtures

refactoring

refactoring

Shared Fixtures

Shared Fixtures

transient fixtures

transient fixtures

Verify One Condition per Test

Verify One Condition per Test

fixture teardown patterns

fixture teardown patterns

Automated Teardown

Automated Teardown

Garbage-Collected Teardown

Garbage-Collected Teardown

Implicit Teardown. See also Implicit Teardown

Implicit Teardown. See also Implicit Teardown

In-line Teardown. See also In-line Teardown

In-line Teardown. See also In-line Teardown

Table Truncation Teardown

Table Truncation Teardown

Transaction Rollback Teardown. See Transaction Rollback Teardown

Transaction Rollback Teardown. See Transaction Rollback Teardown

fixtures

fixtures

collisions

collisions

database testing

database testing

defined

defined

Four-Phase Test

Four-Phase Test

fresh. See Fresh Fixture

fresh. See Fresh Fixture

introduction

introduction

Minimal. See Minimal Fixture

Minimal. See Minimal Fixture

right-sizing Test Methods

right-sizing Test Methods

Shared. See Shared Fixture

Shared. See Shared Fixture

speeding up setup with doubles

speeding up setup with doubles

Standard. See Standard Fixture

Standard. See Standard Fixture

Testcase Class as

Testcase Class as

Testcase Class per Fixture. See Testcase Class per Fixture

Testcase Class per Fixture. See Testcase Class per Fixture

transient. See transient fixtures

transient. See transient fixtures

Flexible Test

Flexible Test

fluent interface

fluent interface

For Tests Only

For Tests Only

foreign-key constraints

foreign-key constraints

forms, pattern

forms, pattern

Four-Phase Test

Four-Phase Test

Custom Assertions

Custom Assertions

fixture design

fixture design

introduction

introduction

Mock Object patterns

Mock Object patterns

pattern description

pattern description

unit testing

unit testing

Verify One Condition per Test

Verify One Condition per Test

Fowler, Martin

Fowler, Martin

code smells

code smells

Creation Methods

Creation Methods

Custom Assertions

Custom Assertions

Cut and Paste code reuse

Cut and Paste code reuse

Delegated Setup

Delegated Setup

Eager Tests solution

Eager Tests solution

Multiple Test Conditions solution

Multiple Test Conditions solution

pattern forms

pattern forms

refactoring

refactoring

refactoring Recorded Tests

refactoring Recorded Tests

reusable test logic

reusable test logic

self-testing code

self-testing code

Standard Fixtures

Standard Fixtures

state vs. behavior verification

state vs. behavior verification

test smells

test smells

Testcase Object exception

Testcase Object exception

Fragile Fixture

Fragile Fixture

defined

defined

introduction

introduction

setUp method misuse

setUp method misuse

Fragile Test

Fragile Test

Behavior Sensitivity

Behavior Sensitivity

Buggy Tests

Buggy Tests

causes

causes

Context Sensitivity

Context Sensitivity

Data Sensitivity

Data Sensitivity

Fragile Fixture

Fragile Fixture

High Test Maintenance Cost

High Test Maintenance Cost

impact

impact

Interface Sensitivity

Interface Sensitivity

introduction

introduction

Overspecified Software

Overspecified Software

Sensitivity Equality

Sensitivity Equality

symptoms

symptoms

troubleshooting

troubleshooting

frameworks

frameworks

Fit. See Fit

Fit. See Fit

Test Automation Framework

Test Automation Framework

Frequent Debugging

Frequent Debugging

avoidance with Custom Assertion

avoidance with Custom Assertion

causes

causes

impact

impact

introduction

introduction

solution patterns

solution patterns

symptoms

symptoms

Fresh Fixture

Fresh Fixture

Creation Method. See Creation Method

Creation Method. See Creation Method

Data Sensitivity solution

Data Sensitivity solution

Delegated Setup

Delegated Setup

example

example

fixture strategies

fixture strategies

implementation

implementation

Implicit Setup

Implicit Setup

Interacting Tests solution

Interacting Tests solution

motivating example

motivating example

Mystery Guests solution

Mystery Guests solution

overview

overview

persistent. See also persistent fixtures

persistent. See also persistent fixtures

refactoring

refactoring

setup

setup

test automation philosophies

test automation philosophies

Test Run Wars solution

Test Run Wars solution

transient. See also transient fixtures

transient. See also transient fixtures

Transient Fresh Fixture

Transient Fresh Fixture

when to use

when to use

front door

front door

Front Door First

Front Door First

defined

defined

Overspecified Software avoidance

Overspecified Software avoidance

Fully Automated Test

Fully Automated Test

behavior smells and

behavior smells and

Communicate Intent and

Communicate Intent and

Manual Fixture Setup solution

Manual Fixture Setup solution

minimizing untested code

minimizing untested code

running

running

unit testing

unit testing

functional tests

functional tests

defined

defined

per-functionality

per-functionality

Fuzzy Equality Assertion

Fuzzy Equality Assertion

defined

defined

example

example

external result verification

external result verification

introduction

introduction

G

G

Gamma, Erich

Gamma, Erich

garbage collection

garbage collection

Garbage-Collected Teardown

Garbage-Collected Teardown

design-for-testability

design-for-testability

pattern description

pattern description

persistent fixtures

persistent fixtures

transient fixtures

transient fixtures

General Fixture

General Fixture

database testing

database testing

defined

defined

misuse of setUp method

misuse of setUp method

Obscure Tests

Obscure Tests

Slow Tests

Slow Tests

Generated Value

Generated Value

Geras, Adam

Geras, Adam

Global Fixture

Global Fixture

global variables

global variables

defined

defined

instance variables as

instance variables as

goals, test automation. See test automation goals

goals, test automation. See test automation goals

Gorts, Sven

Gorts, Sven

granularity

granularity

test automation tools and

test automation tools and

Test-Specific Subclass

Test-Specific Subclass

Graphical Test Runner

Graphical Test Runner

clicking through to test code

clicking through to test code

defined

defined

green bar

green bar

introduction

introduction

graphical user interface (GUI). See GUI (graphical user interface)

graphical user interface (GUI). See GUI (graphical user interface)

green bar, defined

green bar, defined

Guaranteed In-Line Teardown

Guaranteed In-Line Teardown

Guard Assertion

Guard Assertion

Conditional Verification Logic solution

Conditional Verification Logic solution

introduction

introduction

pattern description

pattern description

removing "if" statements in Test Method

removing "if" statements in Test Method

GUI (graphical user interface)

GUI (graphical user interface)

defined

defined

design for testability

design for testability

Interface Sensitivity

Interface Sensitivity

testing with Humble Dialogs

testing with Humble Dialogs

H

H

Hand-Built Test Double. See also Hard-Coded Test Double

Hand-Built Test Double. See also Hard-Coded Test Double

Configurable Test Double

Configurable Test Double

providing

providing

Hand-Coded Mock Object

Hand-Coded Mock Object

hand-coded teardown

hand-coded teardown

Hand-Coded Test Stub

Hand-Coded Test Stub

Hand-Scripted Test. See also Scripted Test

Hand-Scripted Test. See also Scripted Test

introduction

introduction

tools for automating

tools for automating

Hand-Written Test. See Scripted Test

Hand-Written Test. See Scripted Test

happy path

happy path

defined

defined

Responder use

Responder use

Simple Success Tests

Simple Success Tests

test automation roadmap

test automation roadmap

Hard-Coded Mock Object. See Hard-Coded Test Double

Hard-Coded Mock Object. See Hard-Coded Test Double

Hard-Coded Setup Decorator

Hard-Coded Setup Decorator

defined

defined

example

example

Hard-Coded Test Data

Hard-Coded Test Data

causing Obscure Tests

causing Obscure Tests

defined

defined

introduction

introduction

Hard-Coded Test Double

Hard-Coded Test Double

configuring

configuring

implementation

implementation

motivating example

motivating example

naming patterns

naming patterns

overview

overview

refactoring

refactoring

Self Shunt/Loopback

Self Shunt/Loopback

Subclassed Inner Test Double

Subclassed Inner Test Double

Test Double Class

Test Double Class

testing with

testing with

when to use

when to use

Hard-Coded Test Spy. See Hard-Coded Test Double

Hard-Coded Test Spy. See Hard-Coded Test Double

Hard-Coded Test Stub. See also Hard-Coded Test Double

Hard-Coded Test Stub. See also Hard-Coded Test Double

implementation

implementation

indirect input control

indirect input control

Hard-Coded Value

Hard-Coded Value

Hard-To-Test Code

Hard-To-Test Code

Asynchronous Code

Asynchronous Code

Buggy Tests

Buggy Tests

code smells

code smells

Developers Not Writing Tests

Developers Not Writing Tests

divide and test

divide and test

High Test Maintenance Cost

High Test Maintenance Cost

Highly Coupled Code

Highly Coupled Code

impact

impact

solution patterns

solution patterns

symptoms

symptoms

Untestable Test Code

Untestable Test Code

hierarchy of test automation needs

hierarchy of test automation needs

High Test Maintenance Cost

High Test Maintenance Cost

Conditional Test Logic

Conditional Test Logic

In-Line Setup

In-Line Setup

introduction

introduction

smell description

smell description

Higher Level Language

Higher Level Language

Custom Assertion

Custom Assertion

Interface Sensitivity solution

Interface Sensitivity solution

xUnit sweet spot

xUnit sweet spot

Highly Coupled Code

Highly Coupled Code

historical patterns and smells

historical patterns and smells

Hollywood principle

Hollywood principle

defined

defined

test results

test results

Hook, Test. See Test Hook

Hook, Test. See Test Hook

HTML user interface sensitivity

HTML user interface sensitivity

HttpUnit

HttpUnit

Humble Container Adapter

Humble Container Adapter

Humble Dialog

Humble Dialog

design-for-testability

design-for-testability

example

example

Hard-To-Test Code

Hard-To-Test Code

minimizing untested code

minimizing untested code

when to use

when to use

Humble Executable

Humble Executable

asynchronous tests

asynchronous tests

minimizing untested code

minimizing untested code

motivating example

motivating example

Neverfail Test solution

Neverfail Test solution

when to use

when to use

Humble Object

Humble Object

Asynchronous Code solution

Asynchronous Code solution

Humble Dialog

Humble Dialog

Humble Transaction Controller

Humble Transaction Controller

implementation

implementation

motivating example

motivating example

overview

overview

Poor Manís Humble Executable

Poor Manís Humble Executable

refactoring

refactoring

True Humble Executable

True Humble Executable

when to use

when to use

Humble Transaction Controller

Humble Transaction Controller

data access layer testing

data access layer testing

example

example

when to use

when to use

Hurst, John

Hurst, John

hybrid setup

hybrid setup

I

I

IDE (integrated development environment)

IDE (integrated development environment)

defined

defined

introduction

introduction

refactoring

refactoring

Idea

Idea

IeUnit

IeUnit

defined

defined

Graphical Test Runner

Graphical Test Runner

"if" statements

"if" statements

Conditional Test Logic

Conditional Test Logic

Guard Assertions

Guard Assertions

removing

removing

IFixtureFrame

IFixtureFrame

ignoring tests

ignoring tests

Immutable Shared Fixture

Immutable Shared Fixture

defined

defined

example

example

Interacting Tests solution

Interacting Tests solution

introduction

introduction

vs. Irrelevant Information

vs. Irrelevant Information

Test Run Wars solution

Test Run Wars solution

impact

impact

Assertion Roulette

Assertion Roulette

Asynchronous Code

Asynchronous Code

Buggy Tests

Buggy Tests

Conditional Test Logic

Conditional Test Logic

Developers Not Writing Tests

Developers Not Writing Tests

Equality Pollution

Equality Pollution

Erratic Tests

Erratic Tests

Flexible Tests

Flexible Tests

Fragile Tests

Fragile Tests

Frequent Debugging

Frequent Debugging

General Fixtures

General Fixtures

Hard-Coded Test Data

Hard-Coded Test Data

Hard-To-Test Code

Hard-To-Test Code

High Test Maintenance Cost

High Test Maintenance Cost

Highly Coupled Code

Highly Coupled Code

Indirect Testing

Indirect Testing

Irrelevant Information

Irrelevant Information

Manual Intervention

Manual Intervention

Mystery Guests

Mystery Guests

Neverfail Tests

Neverfail Tests

Nondeterministic Tests

Nondeterministic Tests

Obscure Tests

Obscure Tests

Production Bugs

Production Bugs

Slow Tests

Slow Tests

Test Code Duplication

Test Code Duplication

Test Dependency in Production

Test Dependency in Production

Test Hooks

Test Hooks

Test Logic in Production

Test Logic in Production

Test Run Wars

Test Run Wars

For Tests Only

For Tests Only

Untestable Test Code

Untestable Test Code

Untested Requirements

Untested Requirements

Implicit Setup

Implicit Setup

vs. Four-Phase Test

vs. Four-Phase Test

introduction

introduction

matching with teardown code

matching with teardown code

pattern description

pattern description

pattern naming

pattern naming

reusing test code with

reusing test code with

transient fixtures

transient fixtures

Implicit Teardown

Implicit Teardown

Complex Teardown solution

Complex Teardown solution

database

database

vs. Four-Phase Test

vs. Four-Phase Test

pattern description

pattern description

persistent fixtures

persistent fixtures

Self-Checking Tests with

Self-Checking Tests with

Imposter. See Test Double

Imposter. See Test Double

incremental delivery

incremental delivery

agile development

agile development

defined

defined

incremental development

incremental development

defined

defined

test automation philosophies

test automation philosophies

Incremental Tabular Test

Incremental Tabular Test

implementation

implementation

Parameterized Test patterns

Parameterized Test patterns

incremental tests

incremental tests

In-Database Stored Procedure Test

In-Database Stored Procedure Test

database testing

database testing

example

example

implementation

implementation

Independent Tabular Test

Independent Tabular Test

independent testing. See Keep Tests Independent

independent testing. See Keep Tests Independent

indirect input

indirect input

alternative path verification

alternative path verification

controlling

controlling

controlling in Layer Tests

controlling in Layer Tests

defined

defined

importance of

importance of

Test Doubles

Test Doubles

indirect output

indirect output

Behavior Verification. See Behavior Verification

Behavior Verification. See Behavior Verification

defined

defined

importance of

importance of

registries

registries

Test Doubles

Test Doubles

verification

verification

verifying in Layer Tests

verifying in Layer Tests

Indirect Testing

Indirect Testing

defined

defined

Fragile Tests cause

Fragile Tests cause

Obscure Tests cause

Obscure Tests cause

testability

testability

Infrequently Run Test

Infrequently Run Test

Frequent Debugging cause

Frequent Debugging cause

Production Bugs cause

Production Bugs cause

inheritance

inheritance

reusing test code

reusing test code

reusing test fixtures

reusing test fixtures

injected values, Test Stub. See Test Stub

injected values, Test Stub. See Test Stub

Injection, Parameter. See Parameter Injection

Injection, Parameter. See Parameter Injection

in-line Four Phase Test

in-line Four Phase Test

in-line resources

in-line resources

In-line Setup

In-line Setup

introduction

introduction

matching with teardown code

matching with teardown code

Mystery Guest solution

Mystery Guest solution

pattern description

pattern description

transient fixtures

transient fixtures

In-line Teardown

In-line Teardown

examples

examples

implementation

implementation

motivating example

motivating example

Naive In-Line Teardown

Naive In-Line Teardown

overview

overview

of persistent fixtures

of persistent fixtures

refactoring

refactoring

when to use

when to use

In-Memory Database

In-Memory Database

inner class

inner class

anonymous

anonymous

defined

defined

Inner Test Double

Inner Test Double

example

example

Hard-Coded Test Double implementation

Hard-Coded Test Double implementation

Subclassed from Pseudo-Class

Subclassed from Pseudo-Class

Test Spy implementation

Test Spy implementation

input

input

derived

derived

indirect. See indirect input

indirect. See indirect input

naming conventions

naming conventions

inside-out development

inside-out development

vs. outside-in development

vs. outside-in development

State Verification

State Verification

installing Test Doubles

installing Test Doubles

Dependency Injection

Dependency Injection

Dependency Lookup

Dependency Lookup

Fake Object

Fake Object

introduction

introduction

Mock Object

Mock Object

retrofitting testability

retrofitting testability

instance methods

instance methods

defined

defined

with Test Helper

with Test Helper

instance variables

instance variables

converting for Implicit Setup

converting for Implicit Setup

Data-Driven Tests using Fit Framework

Data-Driven Tests using Fit Framework

defined

defined

Fresh Fixtures

Fresh Fixtures

as global variables

as global variables

Reuse Tests for Fixture Setup

Reuse Tests for Fixture Setup

with Test Specific Subclass

with Test Specific Subclass

Testcase Class per Fixture

Testcase Class per Fixture

instances

instances

reusing

reusing

Testcase Object exception

Testcase Object exception

integrated development environment (IDE). See IDE (integrated development environment)

integrated development environment (IDE). See IDE (integrated development environment)

Integration Build

Integration Build

Intent-Revealing Name

Intent-Revealing Name

Custom Assertion

Custom Assertion

Implicit Setup

Implicit Setup

Parameterized Test

Parameterized Test

Test Utility Method

Test Utility Method

Interacting Test Suites

Interacting Test Suites

Interacting Tests

Interacting Tests

avoiding with Database Sandbox

avoiding with Database Sandbox

avoiding with Delta Assertion

avoiding with Delta Assertion

caused by Shared Fixture

caused by Shared Fixture

Chained Tests

Chained Tests

customer testing

customer testing

database testing

database testing

Erratic Test cause

Erratic Test cause

introduction

introduction

Keep Tests Independent

Keep Tests Independent

interaction point

interaction point

interaction styles

interaction styles

Interaction Testing. See Behavior Verification

Interaction Testing. See Behavior Verification

Interface Sensitivity

Interface Sensitivity

defined

defined

introduction

introduction

interfaces

interfaces

Configuration Interface

Configuration Interface

defined

defined

GUI. See GUI (graphical user interface)

GUI. See GUI (graphical user interface)

outgoing interface

outgoing interface

standard test

standard test

Test Runner. See Test Runner

Test Runner. See Test Runner

Use the Front Door First

Use the Front Door First

internal recording tools

internal recording tools

interpreters in Data-Driven Tests. See Data-Driven Test

interpreters in Data-Driven Tests. See Data-Driven Test

Intervention, Manual. See Manual Intervention

Intervention, Manual. See Manual Intervention

Introduce Explaining Variable refactoring

Introduce Explaining Variable refactoring

IoC (inversion of control) framework

IoC (inversion of control) framework

defined

defined

for Dependency Injection

for Dependency Injection

irrelevant information

irrelevant information

defined

defined

Obscure Test

Obscure Test

Isolate the SUT

Isolate the SUT

iterative development

iterative development

J

J

Java

Java

language-specific xUnit terminology

language-specific xUnit terminology

test code packaging

test code packaging

JBehave

JBehave

defined

defined

tests as examples

tests as examples

JFCUnit

JFCUnit

JMock

JMock

Configuration Interface

Configuration Interface

defined

defined

Test Double implementation

Test Double implementation

Johnson, Rod

Johnson, Rod

JUnit

JUnit

defined

defined

Expected Exception Test expression

Expected Exception Test expression

fixture design

fixture design

language-specific terminology

language-specific terminology

Suite Fixture Setup support

Suite Fixture Setup support

Test Automation Framework

Test Automation Framework

test automation tools

test automation tools

Testcase Object exception

Testcase Object exception

testing stored procedures

testing stored procedures

K

K

Keep Test Logic Out of Production Code

Keep Test Logic Out of Production Code

minimizing risk

minimizing risk

principle

principle

test code organization

test code organization

Keep Tests Independent

Keep Tests Independent

running

running

test automation principles

test automation principles

using Fake Object. See Fake Object

using Fake Object. See Fake Object

Kerievsky, Joshua

Kerievsky, Joshua

keys, Literal Values as

keys, Literal Values as

King, Joseph

King, Joseph

L

L

languages

languages

terminology

terminology

variations in Built-in Assertions

variations in Built-in Assertions

xUnit implementations

xUnit implementations

language-specific xUnit terminology

language-specific xUnit terminology

"Law of Raspberry Jam"

"Law of Raspberry Jam"

Layer Test

Layer Test

Business Layer Tests

Business Layer Tests

database testing

database testing

implementation

implementation

motivating example

motivating example

overview

overview

Presentation Layer Tests

Presentation Layer Tests

refactoring

refactoring

Subcutaneous Tests

Subcutaneous Tests

when to use

when to use

layer-crossing tests

layer-crossing tests

defined

defined

testability

testability

Layered Architecture

Layered Architecture

design-for-testability

design-for-testability

layer-crossing tests

layer-crossing tests

Lazy Initialization

Lazy Initialization

Lazy Setup

Lazy Setup

Decorated

Decorated

examples

examples

implementation

implementation

Interacting Tests solution

Interacting Tests solution

motivating example

motivating example

overview

overview

vs. Prebuilt Fixtures

vs. Prebuilt Fixtures

refactoring

refactoring

Shared Fixture

Shared Fixture

when to use

when to use

Lazy Teardown

Lazy Teardown

example

example

implementation

implementation

leakage, resource

leakage, resource

Erratic Tests

Erratic Tests

persistent fixtures

persistent fixtures

learning styles

learning styles

legacy software

legacy software

Buggy Tests

Buggy Tests

defined

defined

tests as safety net

tests as safety net

lenient Mock Object

lenient Mock Object

defined

defined

when to use

when to use

lightweight implementation using Fake Object. See Fake Object

lightweight implementation using Fake Object. See Fake Object

Literal Value

Literal Value

Hard-Coded Test Data

Hard-Coded Test Data

pattern description

pattern description

local variables

local variables

converting in Implicit Setup

converting in Implicit Setup

defined

defined

Fresh Fixtures

Fresh Fixtures

Lonely Test

Lonely Test

caused by Chained Test. See Chained Test

caused by Chained Test. See Chained Test

Erratic Tests

Erratic Tests

Interacting Tests. See Interacting Tests

Interacting Tests. See Interacting Tests

Long Tests. See Obscure Test

Long Tests. See Obscure Test

Loopback. See Self Shunt

Loopback. See Self Shunt

Loop-Driven Test

Loop-Driven Test

implementation

implementation

Parameterized Test

Parameterized Test

loops

loops

as Conditional Test Logic

as Conditional Test Logic

eliminating

eliminating

Production Logic in Test cause

Production Logic in Test cause

Lost Tests

Lost Tests

avoiding

avoiding

Production Bugs cause

Production Bugs cause

M

M

Mackinnon, Tim

Mackinnon, Tim

macros, Assertion Methods as

macros, Assertion Methods as

maintenance

maintenance

High Test Maintenance Cost. See High Test Maintenance Cost

High Test Maintenance Cost. See High Test Maintenance Cost

optimizing

optimizing

test automation goals

test automation goals

Manual Event Injection

Manual Event Injection

Manual Fixture Setup

Manual Fixture Setup

Manual Intervention

Manual Intervention

impact

impact

introduction

introduction

Manual Event Injection

Manual Event Injection

Manual Fixture Setup

Manual Fixture Setup

Manual Result Verification

Manual Result Verification

symptoms

symptoms

Manual Result Verification

Manual Result Verification

manual testing

manual testing

defined

defined

right-sizing Test Methods

right-sizing Test Methods

Marrick, Brian

Marrick, Brian

purpose of tests

purpose of tests

right-sizing Test Methods

right-sizing Test Methods

tests as examples

tests as examples

Maslow

Maslow

MbUnit

MbUnit

defined

defined

Parameterized Test implementation

Parameterized Test implementation

Tabular Test with framework support

Tabular Test with framework support

Message, Assertion. See Assertion Message

Message, Assertion. See Assertion Message

messages, failure. See failure messages

messages, failure. See failure messages

meta objects

meta objects

Data-Driven Tests

Data-Driven Tests

defined

defined

metatests

metatests

method attributes

method attributes

defined

defined

Expected Exception Tests

Expected Exception Tests

Test Discovery using

Test Discovery using

Test Method Selection using

Test Method Selection using

method names

method names

language-specific xUnit terminology

language-specific xUnit terminology

Test Method Discovery

Test Method Discovery

methods

methods

diagramming notation

diagramming notation

instance. See instance methods

instance. See instance methods

setUp. See setUp method

setUp. See setUp method

static

static

suite

suite

tearDown. See tearDown method

tearDown. See tearDown method

Template Method

Template Method

test commands

test commands

verification. See result verification

verification. See result verification

Miller, Jeremy

Miller, Jeremy

Minimal Fixture

Minimal Fixture

external result verification

external result verification

General Fixtures solution

General Fixtures solution

minimizing data

minimizing data

misuse of setUp method

misuse of setUp method

pattern description

pattern description

strategy

strategy

test automation philosophies

test automation philosophies

Minimize Test Overlap

Minimize Test Overlap

Minimize Untestable Code

Minimize Untestable Code

Missing Assertion Message

Missing Assertion Message

Missing Unit Test

Missing Unit Test

Defect Localization

Defect Localization

Production Bugs

Production Bugs

mixins

mixins

defined

defined

Test Helper Mixins

Test Helper Mixins

Mock Object

Mock Object

Configurable. See Configurable Test Double

Configurable. See Configurable Test Double

configuring

configuring

defined

defined

examples

examples

Expected Behavior Specification

Expected Behavior Specification

implementation

implementation

motivating example

motivating example

Overspecified Software cause

Overspecified Software cause

overview

overview

refactoring

refactoring

Test Double patterns

Test Double patterns

Test Doubles

Test Doubles

unit testing

unit testing

vs. Use the Front Door First

vs. Use the Front Door First

verifying indirect output

verifying indirect output

when to use

when to use

xUnit terminology

xUnit terminology

MockMaker

MockMaker

modules

modules

Move Method

Move Method

MSTest

MSTest

Mugridge, Rick

Mugridge, Rick

multimodal tests

multimodal tests

multiple-condition tests

multiple-condition tests

Conditional Test Logic

Conditional Test Logic

defined

defined

Multiresource In-line Teardown

Multiresource In-line Teardown

MySql

MySql

Mystery Guest

Mystery Guest

defined

defined

Obscure Test cause

Obscure Test cause

N

N

Naive In-line Teardown

Naive In-line Teardown

defined

defined

example

example

of persistent fixtures

of persistent fixtures

Naive xUnit Test Interpreter

Naive xUnit Test Interpreter

Named State Reaching Method

Named State Reaching Method

Named Test Suite

Named Test Suite

examples

examples

implementation

implementation

introduction

introduction

overview

overview

refactoring

refactoring

Test Enumeration

Test Enumeration

when to use

when to use

names

names

Dependency Lookup

Dependency Lookup

intent-revealing. See Intent-Revealing Name

intent-revealing. See Intent-Revealing Name

referring to patterns and smells

referring to patterns and smells

Scripted Test

Scripted Test

Suite Fixture Setup

Suite Fixture Setup

naming conventions

naming conventions

assertion-identifying messages

assertion-identifying messages

making resources unique

making resources unique

patterns

patterns

vs. test code organization

vs. test code organization

Test Method Discovery

Test Method Discovery

Testcase Class per Class

Testcase Class per Class

Testcase Class per Feature

Testcase Class per Feature

Testcase Class per Fixture

Testcase Class per Fixture

For Tests Only solution

For Tests Only solution

need-driven development

need-driven development

Behavior Verification

Behavior Verification

defined

defined

testing with doubles

testing with doubles

using Mock Objects

using Mock Objects

Neverfail Test

Neverfail Test

New River Gorge bridge

New River Gorge bridge

Newkirk, James

Newkirk, James

NMock

NMock

No Test Risk

No Test Risk

Nondeterministic Test

Nondeterministic Test

dangers of

dangers of

Erratic Test

Erratic Test

Generated Values cause

Generated Values cause

notation, diagramming

notation, diagramming

Null Object vs. Dummy Object

Null Object vs. Dummy Object

null values in Dummy Objects

null values in Dummy Objects

NUnit

NUnit

defined

defined

Expected Exception Test expression

Expected Exception Test expression

fixture design

fixture design

Interacting Test Suites

Interacting Test Suites

Suite Fixture Setup support

Suite Fixture Setup support

Test Automation Frameworks

Test Automation Frameworks

test automation ways and means

test automation ways and means

test fixtures

test fixtures

Testcase Classes

Testcase Classes

Testcase Object exception

Testcase Object exception

O

O

Object Attribute Equality Assertion

Object Attribute Equality Assertion

Object Factory

Object Factory

Dependency Lookup

Dependency Lookup

installing Test Double

installing Test Double

Object Mother

Object Mother

in Delegated Setup

in Delegated Setup

when to use

when to use

object technology

object technology

Object Transaction Rollback Teardown

Object Transaction Rollback Teardown

object-oriented programming language (OOPL)

object-oriented programming language (OOPL)

object-relational mapping (ORM). See ORM (object-relational mapping)

object-relational mapping (ORM). See ORM (object-relational mapping)

objects

objects

Creation Method. See Creation Method

Creation Method. See Creation Method

determining necessary

determining necessary

diagramming notation

diagramming notation

fake. See Fake Object

fake. See Fake Object

Test Suite Objects. See Test Suite Object

Test Suite Objects. See Test Suite Object

Testcase. See Testcase Object

Testcase. See Testcase Object

Obscure Test

Obscure Test

avoiding with Custom Assertion

avoiding with Custom Assertion

avoiding with Separation of Concerns

avoiding with Separation of Concerns

Buggy Test

Buggy Test

causes

causes

vs. Communicate Intent

vs. Communicate Intent

customer testing

customer testing

database testing

database testing

Eager Test

Eager Test

General Fixture

General Fixture

Hard-Coded Test Data

Hard-Coded Test Data

High Test Maintenance Cost

High Test Maintenance Cost

impact

impact

Indirect Testing

Indirect Testing

introduction

introduction

Irrelevant Information

Irrelevant Information

Mystery Guests

Mystery Guests

optimizing test execution/maintenance

optimizing test execution/maintenance

smells

smells

solution patterns

solution patterns

symptoms

symptoms

observation points

observation points

defined

defined

test automation strategy

test automation strategy

O'Grady, Ted

O'Grady, Ted

One Bad Attribute

One Bad Attribute

example

example

introduction

introduction

Minimal Fixtures

Minimal Fixtures

when to use

when to use

OOPL (object-oriented programming language)

OOPL (object-oriented programming language)

optimism, resource

optimism, resource

order of tests

order of tests

organization, test. See test organization; test organization patterns

organization, test. See test organization; test organization patterns

ORM (object-relational mapping)

ORM (object-relational mapping)

defined

defined

Table Truncation Teardown

Table Truncation Teardown

Table Truncation Teardown using

Table Truncation Teardown using

Transaction Rollback Teardown

Transaction Rollback Teardown

Outcome Assertions, Stated. See Stated Outcome Assertion

Outcome Assertions, Stated. See Stated Outcome Assertion

outcome verification patterns. See result verification patterns

outcome verification patterns. See result verification patterns

outcome-describing Verification Method

outcome-describing Verification Method

outgoing interface

outgoing interface

out-of-order calls

out-of-order calls

output, indirect. See indirect output

output, indirect. See indirect output

outside-in development

outside-in development

Behavior Verification

Behavior Verification

vs. inside-out development

vs. inside-out development

Overcoupled Software

Overcoupled Software

overlapping tests

overlapping tests

minimizing

minimizing

Too Many Tests

Too Many Tests

Overspecified Software

Overspecified Software

avoiding with Fake Objects

avoiding with Fake Objects

Fragile Tests

Fragile Tests

testing with doubles

testing with doubles

Use the Front Door First

Use the Front Door First

P

P

Parameter Injection

Parameter Injection

example

example

implementation

implementation

installing Test Doubles

installing Test Doubles

Parameterized Anonymous Creation Method

Parameterized Anonymous Creation Method

Parameterized Creation Method

Parameterized Creation Method

defined

defined

Delegated Setup

Delegated Setup

example

example

Irrelevant Information solution

Irrelevant Information solution

Parameterized Setup Decorator

Parameterized Setup Decorator

defined

defined

example

example

Parameterized Test

Parameterized Test

example

example

extracting. See Data-Driven Test

extracting. See Data-Driven Test

further reading

further reading

implementation

implementation

Incremental Tabular Test

Incremental Tabular Test

Independent Tabular Test

Independent Tabular Test

Loop-Driven Tests

Loop-Driven Tests

motivating example

motivating example

overview

overview

reducing Test Code Duplication

reducing Test Code Duplication

refactoring

refactoring

Tabular Test with framework support

Tabular Test with framework support

Test Utility Method

Test Utility Method

when to use

when to use

parameters, arguments as

parameters, arguments as

"Pass-Fail-Fail"

"Pass-Fail-Fail"

pattern language

pattern language

defined

defined

pattern naming

pattern naming

Pattern Languages of Programming (PLoP)

Pattern Languages of Programming (PLoP)

patterns

patterns

aliases and variations

aliases and variations

database. See database patterns

database. See database patterns

defined

defined

design-for-testability. See design-for-testability patterns

design-for-testability. See design-for-testability patterns

fixture setup. See fixture setup patterns

fixture setup. See fixture setup patterns

result verification. See result verification patterns

result verification. See result verification patterns

test automation introduction

test automation introduction

Test Double. See Test Double

Test Double. See Test Double

test organization. See test organization patterns

test organization. See test organization patterns

test strategy. See test strategy patterns

test strategy. See test strategy patterns

testability

testability

value. See value patterns

value. See value patterns

xUnit basics. See xUnit basics patterns

xUnit basics. See xUnit basics patterns

peeling the onion

peeling the onion

per-functionality test

per-functionality test

Perrotta, Paolo

Perrotta, Paolo

Per-Run Fixtures

Per-Run Fixtures

persistence layer

persistence layer

persistence resources

persistence resources

persistent fixtures

persistent fixtures

database testing

database testing

issues caused by

issues caused by

managing

managing

overview

overview

Slow Tests cause

Slow Tests cause

Table Truncation Teardown. See Table Truncation Teardown

Table Truncation Teardown. See Table Truncation Teardown

teardown avoidance

teardown avoidance

tearing down

tearing down

test strategy patterns

test strategy patterns

what's next

what's next

Persistent Fresh Fixture

Persistent Fresh Fixture

building

building

defined

defined

strategies

strategies

Personal Oracle

Personal Oracle

philosophy, test automation. See test automation philosophies

philosophy, test automation. See test automation philosophies

PHPUnit

PHPUnit

PLoP (Pattern Languages of Programming)

PLoP (Pattern Languages of Programming)

Pluggable Behavior

Pluggable Behavior

in Named Test Suites

in Named Test Suites

Testcase Object implementation

Testcase Object implementation

pollution

pollution

Equality Pollution

Equality Pollution

Shared Fixture

Shared Fixture

polymorphism

polymorphism

Poor Manís Humble Executable

Poor Manís Humble Executable

Poor Man's Humble Object

Poor Man's Humble Object

implementation

implementation

Transaction Rollback Teardown

Transaction Rollback Teardown

Poppendieck, Mary

Poppendieck, Mary

Pragmatic Unit Testing

Pragmatic Unit Testing

Prebuilt Fixture

Prebuilt Fixture

examples

examples

implementation

implementation

motivating example

motivating example

overview

overview

refactoring

refactoring

Shared Fixture strategies

Shared Fixture strategies

Shared Fixtures

Shared Fixtures

Unrepeatable Tests cause

Unrepeatable Tests cause

presentation layer

presentation layer

defined

defined

Layer Tests example

Layer Tests example

testing

testing

presentation logic

presentation logic

Preserve Whole Object refactoring

Preserve Whole Object refactoring

principles

principles

list of

list of

patterns vs.

patterns vs.

test automation. See test automation principles

test automation. See test automation principles

Private Fixture. See Fresh Fixture

Private Fixture. See Fresh Fixture

private methods

private methods

problem statements

problem statements

Procedural Behavior Verification

Procedural Behavior Verification

defined

defined

example

example

indirect outputs

indirect outputs

introduction

introduction

Test Spy usage

Test Spy usage

Procedural State Verification

Procedural State Verification

defined

defined

example

example

introduction

introduction

Procedural Test Stub

Procedural Test Stub

defined

defined

introduction

introduction

when to use

when to use

Procedure Test, Stored. See Stored Procedure Test

Procedure Test, Stored. See Stored Procedure Test

procedure variables

procedure variables

production

production

Production Bugs

Production Bugs

Infrequently Run Tests

Infrequently Run Tests

introduction

introduction

Lost Tests

Lost Tests

Missing Unit Tests

Missing Unit Tests

Neverfail Tests

Neverfail Tests

overview

overview

reducing risk

reducing risk

Untested Code

Untested Code

Untested Requirements

Untested Requirements

production code

production code

defined

defined

keeping test logic out of

keeping test logic out of

Production Logic in Test

Production Logic in Test

profiling tools

profiling tools

Programmatic Test. See Scripted Test

Programmatic Test. See Scripted Test

programmer tests

programmer tests

project smells

project smells

Buggy Tests

Buggy Tests

defined

defined

Developers Not Writing Tests

Developers Not Writing Tests

High Test Maintenance Cost

High Test Maintenance Cost

overview

overview

Production Bugs. See Production Bugs

Production Bugs. See Production Bugs

property tests

property tests

Pseudo-Object

Pseudo-Object

Hard-Coded Test Double implementation

Hard-Coded Test Double implementation

Inner Test Double Subclassed from Pseudo-Class

Inner Test Double Subclassed from Pseudo-Class

testing with doubles

testing with doubles

pull system

pull system

Pull-Up Method refactoring

Pull-Up Method refactoring

Delegated Setup

Delegated Setup

moving reusable test logic

moving reusable test logic

Testcase Superclass

Testcase Superclass

Pushdown Decorator

Pushdown Decorator

PyUnit

PyUnit

defined

defined

Test Automation Framework

Test Automation Framework

Q

Q

QA (quality assurance)

QA (quality assurance)

QaRun

QaRun

QTP (QuickTest Professional)

QTP (QuickTest Professional)

Data-Driven Tests

Data-Driven Tests

defined

defined

record and playback tools

record and playback tools

Test Automation Framework

Test Automation Framework

quality assurance (QA)

quality assurance (QA)

QuickTest Professional (QTP). See QTP (QuickTest Professional)

QuickTest Professional (QTP). See QTP (QuickTest Professional)

R

R

random values

random values

Nondeterministic Tests

Nondeterministic Tests

Random Generated Values

Random Generated Values

Record and Playback Test

Record and Playback Test

record and playback tools

record and playback tools

introduction

introduction

Recorded Tests

Recorded Tests

xUnit sweet spot

xUnit sweet spot

Recorded Test

Recorded Test

built-in test recording

built-in test recording

commercial record and playback tool

commercial record and playback tool

customer testing

customer testing

Data-Driven Tests and

Data-Driven Tests and

implementation

implementation

Interface Sensitivity

Interface Sensitivity

overview

overview

refactored commercial recorded tests

refactored commercial recorded tests

vs. Scripted Tests

vs. Scripted Tests

smells

smells

tools

tools

tools for automating

tools for automating

when to use

when to use

Recording Test Stub. See Test Spy

Recording Test Stub. See Test Spy

red bar

red bar

Refactored Recorded Tests

Refactored Recorded Tests

commercial

commercial

overview

overview

refactoring. See also test refactorings

refactoring. See also test refactorings

Assertion Message

Assertion Message

Assertion Method

Assertion Method

Automated Teardown

Automated Teardown

Back Door Manipulation

Back Door Manipulation

Chained Test

Chained Test

Configurable Test Double

Configurable Test Double

Creation Method

Creation Method

Custom Assertion

Custom Assertion

Database Sandbox

Database Sandbox

Data-Driven Test

Data-Driven Test

defined

defined

Delegated Setup

Delegated Setup

Delta Assertion

Delta Assertion

Dependency Injection

Dependency Injection

Dependency Lookup

Dependency Lookup

Derived Value

Derived Value

Dummy Object

Dummy Object

Fake Object

Fake Object

Fresh Fixture

Fresh Fixture

Garbage-Collected Teardown

Garbage-Collected Teardown

Generated Value

Generated Value

Guard Assertion

Guard Assertion

Hard-Coded Test Double

Hard-Coded Test Double

Humble Object

Humble Object

Implicit Setup

Implicit Setup

Implicit Teardown

Implicit Teardown

In-line Setup

In-line Setup

In-line Teardown

In-line Teardown

Layer Test

Layer Test

Lazy Setup

Lazy Setup

Literal Value

Literal Value

Mock Object

Mock Object

Named Test Suite

Named Test Suite

Parameterized Test

Parameterized Test

Prebuilt Fixture

Prebuilt Fixture

Setup Decorator

Setup Decorator

Shared Fixture

Shared Fixture

Standard Fixture

Standard Fixture

State Verification

State Verification

Stored Procedure Test

Stored Procedure Test

Suite Fixture Setup

Suite Fixture Setup

Table Truncation Teardown

Table Truncation Teardown

Test Discovery

Test Discovery

Test Helper

Test Helper

Test Spy

Test Spy

Test Stub

Test Stub

Test Utility Method

Test Utility Method

Testcase Class per Feature

Testcase Class per Feature

Testcase Class per Fixture

Testcase Class per Fixture

Testcase Superclass

Testcase Superclass

Test-Specific Subclass

Test-Specific Subclass

Transaction Rollback Teardown

Transaction Rollback Teardown

Unfinished Test Assertion

Unfinished Test Assertion

Refactoring: Improving the Design of Existing Code (Fowler)

Refactoring: Improving the Design of Existing Code (Fowler)

references

references

reflection

reflection

defined

defined

Test Discovery

Test Discovery

Testcase Object implementation

Testcase Object implementation

Registry

Registry

configurable

configurable

in Dependency Lookup

in Dependency Lookup

Interacting Tests

Interacting Tests

Test Fixture

Test Fixture

regression tests

regression tests

defined

defined

Recorded Tests. See Recorded Test

Recorded Tests. See Recorded Test

Scripted Tests

Scripted Tests

Related Generated Values

Related Generated Values

example

example

implementation

implementation

Remoted Stored Procedure Test

Remoted Stored Procedure Test

example

example

implementation

implementation

introduction

introduction

Repeatable Test

Repeatable Test

defined

defined

indirect inputs control

indirect inputs control

Replace Dependency with Test Double refactoring

Replace Dependency with Test Double refactoring

Behavior Verification

Behavior Verification

defined

defined

Repository

Repository

Data-Driven Test files

Data-Driven Test files

persistent objects

persistent objects

source code

source code

test code

test code

Requirement, Untested. See Untested Requirement

Requirement, Untested. See Untested Requirement

ReSharper

ReSharper

Resource Leakage

Resource Leakage

Erratic Tests

Erratic Tests

persistent fixtures

persistent fixtures

Resource Optimism

Resource Optimism

resources

resources

external

external

in-line

in-line

unique

unique

Responder

Responder

defined

defined

examples

examples

indirect input control

indirect input control

introduction

introduction

when to use

when to use

response time tests

response time tests

result verification

result verification

Behavior Verification

Behavior Verification

Conditional Test Logic avoidance

Conditional Test Logic avoidance

Data Sensitivity

Data Sensitivity

defined

defined

Four-Phase Test

Four-Phase Test

Mock Object

Mock Object

other techniques

other techniques

reducing Test Code Duplication

reducing Test Code Duplication

reusable test logic

reusable test logic

Self-Checking Tests

Self-Checking Tests

State Verification

State Verification

result verification patterns

result verification patterns

Behavior Verification. See Behavior Verification

Behavior Verification. See Behavior Verification

Custom Assertion. See Custom Assertion

Custom Assertion. See Custom Assertion

Delta Assertion

Delta Assertion

Guard Assertion

Guard Assertion

State Verification. See State Verification

State Verification. See State Verification

Unfinished Test Assertion

Unfinished Test Assertion

results, test

results, test

defined

defined

introduction

introduction

Retrieval Interface

Retrieval Interface

retrospective

retrospective

reusable test logic

reusable test logic

Creation Method

Creation Method

fixture setup patterns

fixture setup patterns

organization

organization

result verification

result verification

Test Code Duplication

Test Code Duplication

Test Utility Method. See Test Utility Method

Test Utility Method. See Test Utility Method

Reuse Tests for Fixture Setup

Reuse Tests for Fixture Setup

Robot User Test. See Recorded Test

Robot User Test. See Recorded Test

robot user tools

robot user tools

defined

defined

introduction

introduction

Test Automation Framework

Test Automation Framework

Robust Tests

Robust Tests

defined

defined

indirect inputs control

indirect inputs control

role-describing arguments

role-describing arguments

root cause analysis

root cause analysis

defined

defined

smells

smells

round-trip tests

round-trip tests

defined

defined

introduction

introduction

Layer Tests

Layer Tests

row tests. See Tabular Test

row tests. See Tabular Test

RSpec

RSpec

defined

defined

fixture design

fixture design

tests as examples

tests as examples

runit

runit

defined

defined

Test Automation Frameworks

Test Automation Frameworks

running tests

running tests

introduction

introduction

structure

structure

test automation goals

test automation goals

runtime reflection

runtime reflection

S

S

Saboteur

Saboteur

defined

defined

example

example

inside-out development

inside-out development

Test Double patterns

Test Double patterns

when to use

when to use

Safety Net

Safety Net

Buggy Tests

Buggy Tests

tests as

tests as

sample code

sample code

screen scraping

screen scraping

Scripted Test

Scripted Test

Communicate Intent

Communicate Intent

customer testing

customer testing

Data-Driven Tests and

Data-Driven Tests and

introduction

introduction

pattern description

pattern description

vs. Recorded Tests

vs. Recorded Tests

smells

smells

UI

UI

Verify One Condition per Test

Verify One Condition per Test

Self Shunt

Self Shunt

Behavior Verifications

Behavior Verifications

example

example

Hard-Coded Test Double implementation

Hard-Coded Test Double implementation

pattern naming

pattern naming

Test Spy implementation

Test Spy implementation

Self-Call

Self-Call

Self-Checking Test

Self-Checking Test

Assertion Method usage

Assertion Method usage

Conditional Test Logic solution

Conditional Test Logic solution

defined

defined

happy path code

happy path code

introduction

introduction

running

running

Self-Describing Value

Self-Describing Value

example

example

Literal Value patterns

Literal Value patterns

self-testing code

self-testing code

self-tests, built-in

self-tests, built-in

defined

defined

test file organization

test file organization

Sensitive Equality

Sensitive Equality

Fragile Tests

Fragile Tests

test-first development

test-first development

sensitivities

sensitivities

automated unit testing

automated unit testing

behavior. See Behavior Sensitivity

behavior. See Behavior Sensitivity

Buggy Tests cause

Buggy Tests cause

context. See Context Sensitivity

context. See Context Sensitivity

data. See Data Sensitivity

data. See Data Sensitivity

interface. See Interface Sensitivity

interface. See Interface Sensitivity

Separation of Concerns

Separation of Concerns

Service Facade

Service Facade

service layers

service layers

fake

fake

tests

tests

Service Locator

Service Locator

in Dependency Lookup. See Dependency Lookup

in Dependency Lookup. See Dependency Lookup

installing Test Doubles

installing Test Doubles

service objects

service objects

Setter Injection

Setter Injection

Configuration Interface using

Configuration Interface using

example

example

implementation

implementation

installing Test Doubles

installing Test Doubles

setters

setters

setup, fixtures. See fixture setup

setup, fixtures. See fixture setup

Setup Decorator

Setup Decorator

examples

examples

implementation

implementation

Implicit Setup

Implicit Setup

motivating example

motivating example

overview

overview

refactoring

refactoring

Shared Fixture strategies

Shared Fixture strategies

when to use

when to use

setUp method

setUp method

Implicit Setup

Implicit Setup

misuse of

misuse of

pattern naming

pattern naming

Setup Decorator. See Setup Decorator

Setup Decorator. See Setup Decorator

Suite Fixture Setup. See Suite Fixture Setup

Suite Fixture Setup. See Suite Fixture Setup

shadows, diagramming notation

shadows, diagramming notation

Shank, Clint

Shank, Clint

Shared Fixture. See also Standard Fixture

Shared Fixture. See also Standard Fixture

Behavior Verification

Behavior Verification

Chained Test. See Chained Test

Chained Test. See Chained Test

customer testing

customer testing

Data Sensitivity cause

Data Sensitivity cause

database testing

database testing

defined

defined

Delta Assertions

Delta Assertions

example

example

Immutable. See Immutable Shared Fixture

Immutable. See Immutable Shared Fixture

Immutable Shared Fixtures

Immutable Shared Fixtures

implementation

implementation

incremental tests

incremental tests

Interacting Tests cause

Interacting Tests cause

introduction

introduction

Lazy Setup. See Lazy Setup

Lazy Setup. See Lazy Setup

managing

managing

motivating example

motivating example

in Nondeterministic Tests

in Nondeterministic Tests

overview

overview

Prebuilt Fixture. See Prebuilt Fixture

Prebuilt Fixture. See Prebuilt Fixture

refactoring

refactoring

Setup Decorator. See Setup Decorator

Setup Decorator. See Setup Decorator

Slow Tests cause

Slow Tests cause

Suite Fixture Setup. See Suite Fixture Setup

Suite Fixture Setup. See Suite Fixture Setup

Table Truncation Teardown. See Table Truncation Teardown

Table Truncation Teardown. See Table Truncation Teardown

Test Run Wars cause

Test Run Wars cause

Unrepeatable Tests cause

Unrepeatable Tests cause

using Finder Methods

using Finder Methods

when to use

when to use

Shared Fixture Guard Assertion

Shared Fixture Guard Assertion

Shared Fixture State Assertion

Shared Fixture State Assertion

Simple Success Test

Simple Success Test

example

example

happy path code

happy path code

introduction

introduction

pattern description

pattern description

The simplest thing that could possibly work (STTCPW)

The simplest thing that could possibly work (STTCPW)

Single Glance Readable. See Communicate Intent

Single Glance Readable. See Communicate Intent

Single Layer Test. See Layer Test

Single Layer Test. See Layer Test

Single Test Suite

Single Test Suite

example

example

Lost Tests solution

Lost Tests solution

when to use

when to use

single tests

single tests

Single-Condition Test

Single-Condition Test

Eager Tests solution

Eager Tests solution

Obscure Tests solution

Obscure Tests solution

principles. See Verify One Condition per Test

principles. See Verify One Condition per Test

unit testing

unit testing

Single-Outcome Assertion

Single-Outcome Assertion

Assertion Method

Assertion Method

defined

defined

example

example

Singleton

Singleton

in Dependency Lookup

in Dependency Lookup

Interacting Tests

Interacting Tests

retrofitting testability

retrofitting testability

Singleton, Substituted

Singleton, Substituted

example

example

when to use

when to use

skeletons

skeletons

Slow Component Usage

Slow Component Usage

Slow Tests

Slow Tests

Asynchronous Tests

Asynchronous Tests

avoiding with Shared Fixture

avoiding with Shared Fixture

database testing

database testing

design for testability

design for testability

due to Transaction Rollback Teardown

due to Transaction Rollback Teardown

General Fixtures

General Fixtures

impact

impact

introduction

introduction

optimizing execution

optimizing execution

persistent fixtures

persistent fixtures

preventing with Fake Object. See Fake Object

preventing with Fake Object. See Fake Object

preventing with Test Double

preventing with Test Double

Slow Component Usage

Slow Component Usage

symptoms

symptoms

Too Many Tests

Too Many Tests

troubleshooting

troubleshooting

smells, test. See test smells

smells, test. See test smells

Smith, Shaun

Smith, Shaun

Smoke Test

Smoke Test

development process

development process

suites

suites

Test Discovery

Test Discovery

sniff test

sniff test

defined

defined

test smells

test smells

solution patterns, behavior smells

solution patterns, behavior smells

Asynchronous Tests

Asynchronous Tests

Behavior Sensitivity

Behavior Sensitivity

Context Sensitivity

Context Sensitivity

Data Sensitivity

Data Sensitivity

Eager Tests

Eager Tests

Frequent Debugging

Frequent Debugging

General Fixture

General Fixture

Interacting Test Suites

Interacting Test Suites

Interacting Tests

Interacting Tests

Interface Sensitivity

Interface Sensitivity

Manual Intervention

Manual Intervention

Missing Assertion Messages

Missing Assertion Messages

Resource Leakage

Resource Leakage

Resource Optimism

Resource Optimism

Slow Component Usage

Slow Component Usage

Test Run War

Test Run War

Too Many Tests

Too Many Tests

Unrepeatable Tests

Unrepeatable Tests

solution patterns, code smells

solution patterns, code smells

Asynchronous Code

Asynchronous Code

Conditional Verification Logic

Conditional Verification Logic

Cut and Paste code reuse

Cut and Paste code reuse

Eager Test

Eager Test

Equality Pollution

Equality Pollution

Flexible Test

Flexible Test

General Fixture

General Fixture

Hard-Coded Test Data

Hard-Coded Test Data

Hard-To-Test Code

Hard-To-Test Code

Highly Coupled Code

Highly Coupled Code

Indirect Testing

Indirect Testing

Irrelevant Information

Irrelevant Information

Multiple Test Conditions

Multiple Test Conditions

Mystery Guests

Mystery Guests

Obscure Tests

Obscure Tests

Production Logic in Test

Production Logic in Test

Test Code Duplication

Test Code Duplication

Test Dependency in Production

Test Dependency in Production

Test Hook

Test Hook

For Tests Only

For Tests Only

Untestable Test Code

Untestable Test Code

solution patterns, project smells

solution patterns, project smells

Buggy Test

Buggy Test

Infrequently Run Test

Infrequently Run Test

Lost Test

Lost Test

Missing Unit Test

Missing Unit Test

Neverfail Test

Neverfail Test

Untested Code

Untested Code

Untested Requirements

Untested Requirements

Special-Purpose Suite

Special-Purpose Suite

specification

specification

Expected Behavior

Expected Behavior

Expected Behavior example

Expected Behavior example

Expected Object example

Expected Object example

Expected State

Expected State

tests as

tests as

spikes

spikes

Spy, Test. See Test Spy

Spy, Test. See Test Spy

SQL, Table Truncation Teardown using

SQL, Table Truncation Teardown using

Standard Fixture

Standard Fixture

implementation

implementation

motivating example

motivating example

overview

overview

refactoring

refactoring

when to use

when to use

standard test interface

standard test interface

starbursts, diagramming notation

starbursts, diagramming notation

state, initializing via

state, initializing via

Back Door Manipulation. See Back Door Manipulation

Back Door Manipulation. See Back Door Manipulation

Named State Reaching Method

Named State Reaching Method

State Verification

State Verification

vs. behavior

vs. behavior

examples

examples

implementation

implementation

indirect outputs

indirect outputs

introduction

introduction

motivating example

motivating example

overview

overview

refactoring

refactoring

Self-Checking Tests

Self-Checking Tests

Use the Front Door First

Use the Front Door First

when to use

when to use

Stated Outcome Assertion

Stated Outcome Assertion

Assertion Methods

Assertion Methods

defined

defined

example

example

Guard Assertions as

Guard Assertions as

introduction

introduction

State-Exposing Subclass

State-Exposing Subclass

Test-Specific Subclass

Test-Specific Subclass

when to use

when to use

stateless

stateless

statements, "if". See "if" statements

statements, "if". See "if" statements

static binding

static binding

defined

defined

Dependency Injection

Dependency Injection

static methods

static methods

static variables

static variables

Statically Generated Test Doubles

Statically Generated Test Doubles

STDD (storytest-driven development)

STDD (storytest-driven development)

stop on first failure

stop on first failure

Naive xUnit Test Interpreter

Naive xUnit Test Interpreter

xUnit introduction

xUnit introduction

Stored Procedure Test

Stored Procedure Test

database testing

database testing

examples

examples

implementation

implementation

motivating example

motivating example

overview

overview

refactoring

refactoring

when to use

when to use

storytest

storytest

storytest-driven development (STDD)

storytest-driven development (STDD)

strategies, test automation. See test automation strategies

strategies, test automation. See test automation strategies

stress tests, cross-functionality

stress tests, cross-functionality

strict Mock Object

strict Mock Object

defined

defined

when to use

when to use

STTCPW (The simplest thing that could possibly work)

STTCPW (The simplest thing that could possibly work)

Stub, Test. See Test Stub

Stub, Test. See Test Stub

Subclass, Test-Specific. See Test-Specific Subclass

Subclass, Test-Specific. See Test-Specific Subclass

Subclassed Humble Object

Subclassed Humble Object

Subclassed Inner Test Double

Subclassed Inner Test Double

Subclassed Singleton

Subclassed Singleton

Subclassed Test Double

Subclassed Test Double

Subcutaneous Test

Subcutaneous Test

customer testing

customer testing

database testing

database testing

design for testability

design for testability

Layer Tests

Layer Tests

Subset Suite

Subset Suite

example

example

implementation

implementation

introduction

introduction

overview

overview

Too Many Tests solution

Too Many Tests solution

when to use

when to use

substitutable dependencies

substitutable dependencies

defined

defined

Dependency Initialization Test

Dependency Initialization Test

using Test Spy

using Test Spy

Substitutable Singleton

Substitutable Singleton

in Dependency Lookup

in Dependency Lookup

example

example

retrofitting testability

retrofitting testability

when to use

when to use

substitution mechanisms

substitution mechanisms

Suite Fixture Setup

Suite Fixture Setup

example

example

implementation

implementation

implicit

implicit

motivating example

motivating example

overview

overview

refactoring

refactoring

Shared Fixture strategies

Shared Fixture strategies

Shared Fixtures

Shared Fixtures

when to use

when to use

suite method

suite method

suites

suites

Named Test Suite. See Named Test Suite

Named Test Suite. See Named Test Suite

test organization

test organization

Test Suite Object. See Test Suite Object

Test Suite Object. See Test Suite Object

Suites of Suites

Suites of Suites

building with Test enumeration

building with Test enumeration

defined

defined

example

example

Interacting Test Suites

Interacting Test Suites

introduction

introduction

SUnit

SUnit

defined

defined

Test Automation Frameworks

Test Automation Frameworks

Superclass, Testcase. See Testcase Superclass

Superclass, Testcase. See Testcase Superclass

SUT (system under test)

SUT (system under test)

control points and observation points

control points and observation points

dangers of modifying

dangers of modifying

defined

defined

Four-Phase Test

Four-Phase Test

interface sensitivity

interface sensitivity

isolation principle

isolation principle

minimizing risk

minimizing risk

preface

preface

replacing in Parameterized Test

replacing in Parameterized Test

result verification. See result verification

result verification. See result verification

state vs. behavior verification

state vs. behavior verification

terminology

terminology

test automation tools

test automation tools

Test Hook in

Test Hook in

understanding with test automation

understanding with test automation

SUT API Encapsulation

SUT API Encapsulation

Chained Tests as

Chained Tests as

Indirect Testing solution

Indirect Testing solution

Interface Sensitivity solution

Interface Sensitivity solution

SUT Encapsulation Method

SUT Encapsulation Method

Symbolic Constants

Symbolic Constants

example

example

Literal Value

Literal Value

symptoms, behavior smells

symptoms, behavior smells

Assertion Roulette

Assertion Roulette

Asynchronous Tests

Asynchronous Tests

Behavior Sensitivity

Behavior Sensitivity

Context Sensitivity

Context Sensitivity

Data Sensitivity

Data Sensitivity

Eager Tests

Eager Tests

Erratic Tests

Erratic Tests

Fragile Tests

Fragile Tests

Frequent Debugging

Frequent Debugging

General Fixtures

General Fixtures

Interacting Test Suites

Interacting Test Suites

Interacting Tests

Interacting Tests

Interface Sensitivity

Interface Sensitivity

Manual Intervention

Manual Intervention

Missing Assertion Messages

Missing Assertion Messages

Nondeterministic Tests

Nondeterministic Tests

Resource Leakage

Resource Leakage

Resource Optimism

Resource Optimism

Slow Tests

Slow Tests

Test Run Wars

Test Run Wars

Too Many Tests

Too Many Tests

Unrepeatable Tests

Unrepeatable Tests

symptoms, code smells

symptoms, code smells

Asynchronous Code

Asynchronous Code

Complex Teardown

Complex Teardown

Conditional Test Logic

Conditional Test Logic

Eager Tests

Eager Tests

Equality Pollution

Equality Pollution

Flexible Tests

Flexible Tests

General Fixtures

General Fixtures

Hard-Coded Test Data

Hard-Coded Test Data

Hard-To-Test Code

Hard-To-Test Code

Highly Coupled Code

Highly Coupled Code

Indirect Testing

Indirect Testing

Irrelevant Information

Irrelevant Information

Multiple Test Conditions

Multiple Test Conditions

Mystery Guests

Mystery Guests

Obscure Tests

Obscure Tests

Production Logic in Test

Production Logic in Test

Test Code Duplication

Test Code Duplication

Test Dependency in Production

Test Dependency in Production

Test Logic in Production

Test Logic in Production

test smells

test smells

For Tests Only

For Tests Only

Untestable Test Code

Untestable Test Code

symptoms, project smells

symptoms, project smells

Buggy Tests

Buggy Tests

Developers Not Writing Tests

Developers Not Writing Tests

High Test Maintenance Cost

High Test Maintenance Cost

Infrequently Run Tests

Infrequently Run Tests

Lost Tests

Lost Tests

Missing Unit Tests

Missing Unit Tests

Neverfail Tests

Neverfail Tests

Production Bugs

Production Bugs

Untested Code

Untested Code

Untested Requirements

Untested Requirements

symptoms, test smells

symptoms, test smells

synchronous tests

synchronous tests

avoiding with Humble Object

avoiding with Humble Object

defined

defined

system under test (SUT). See SUT (system under test)

system under test (SUT). See SUT (system under test)

T

T

Table Truncation Teardown

Table Truncation Teardown

data access layer testing

data access layer testing

defined

defined

examples

examples

implementation

implementation

motivating example

motivating example

overview

overview

refactoring

refactoring

when to use

when to use

tabular data

tabular data

Tabular Test

Tabular Test

Chained Tests

Chained Tests

with framework support

with framework support

implementation

implementation

Incremental

Incremental

Independent

Independent

tasks

tasks

TDD (test-driven development)

TDD (test-driven development)

defined

defined

implementing utility methods

implementing utility methods

introduction

introduction

Missing Unit Tests

Missing Unit Tests

need-driven development

need-driven development

process

process

Test Automation Frameworks

Test Automation Frameworks

test automation principles

test automation principles

teardown, fixture. See fixture teardown

teardown, fixture. See fixture teardown

Teardown Guard Clause

Teardown Guard Clause

example

example

Implicit Teardown

Implicit Teardown

In-line Teardown

In-line Teardown

tearDown method

tearDown method

Implicit Teardown

Implicit Teardown

persistent fixtures

persistent fixtures

Setup Decorator. See Setup Decorator

Setup Decorator. See Setup Decorator

Template Method

Template Method

Temporary Test Stub

Temporary Test Stub

when to use

when to use

xUnit terminology

xUnit terminology

terminology

terminology

test automation introduction

test automation introduction

transient fixtures

transient fixtures

xUnit. See xUnit basics

xUnit. See xUnit basics

test automater

test automater

test automation

test automation

assumptions

assumptions

automated unit testing

automated unit testing

brief tour

brief tour

code samples

code samples

developer testing

developer testing

diagramming notation

diagramming notation

feedback

feedback

fragile test problem

fragile test problem

limitations

limitations

overview

overview

patterns

patterns

refactoring

refactoring

terminology

terminology

testing

testing

uses of

uses of

Test Automation Framework

Test Automation Framework

introduction

introduction

pattern description

pattern description

test automation goals

test automation goals

ease of running

ease of running

improving quality

improving quality

list of

list of

objectives

objectives

reducing risk

reducing risk

system evolution

system evolution

understanding SUT

understanding SUT

why test?

why test?

writing and maintaining

writing and maintaining

Test Automation Manifesto

Test Automation Manifesto

test automation philosophies

test automation philosophies

author's

author's

differences

differences

importance of

importance of

test automation principles

test automation principles

Communicate Intent

Communicate Intent

Design for Testability

Design for Testability

Don't Modify the SUT

Don't Modify the SUT

Ensure Commensurate Effort and Responsibility

Ensure Commensurate Effort and Responsibility

Isolate the SUT

Isolate the SUT

Keep Test Logic Out of Production Code

Keep Test Logic Out of Production Code

Keep Tests Independent

Keep Tests Independent

Minimize Test Overlap

Minimize Test Overlap

Minimize Untestable Code

Minimize Untestable Code

overview

overview

Test Concerns Separately

Test Concerns Separately

Use the Front Door First

Use the Front Door First

Verify One Condition per Test

Verify One Condition per Test

Write the Tests First

Write the Tests First

test automation roadmap

test automation roadmap

alternative path verification

alternative path verification

difficulties

difficulties

direct output verification

direct output verification

execution and maintenance optimization

execution and maintenance optimization

happy path code

happy path code

indirect outputs verification

indirect outputs verification

maintainability

maintainability

test automation strategies

test automation strategies

brief tour

brief tour

control points and observation points

control points and observation points

cross-functional tests

cross-functional tests

divide and test

divide and test

ensuring testability

ensuring testability

fixture strategies overview

fixture strategies overview

interaction styles and testability patterns

interaction styles and testability patterns

overview

overview

per-functionality tests

per-functionality tests

persistent fresh fixtures

persistent fresh fixtures

shared fixture strategies

shared fixture strategies

test-driven testability

test-driven testability

tools for

tools for

transient fresh fixtures

transient fresh fixtures

what's next

what's next

wrong

wrong

Test Bed. See Prebuilt Fixture

Test Bed. See Prebuilt Fixture

test cases

test cases

test code

test code

Test Code Duplication

Test Code Duplication

causes

causes

Custom Assertions

Custom Assertions

Delegated Setup

Delegated Setup

High Test Maintenance Cost

High Test Maintenance Cost

impact

impact

In-Line Setup

In-Line Setup

introduction

introduction

possible solution

possible solution

reducing

reducing

reducing with Configurable Test Doubles. See Configurable Test Double

reducing with Configurable Test Doubles. See Configurable Test Double

reducing with Parameterized Tests. See Parameterized Test

reducing with Parameterized Tests. See Parameterized Test

reducing with Test Utility Methods. See Test Utility Method

reducing with Test Utility Methods. See Test Utility Method

removing with Testcase Class per Fixture. See Testcase Class per Fixture

removing with Testcase Class per Fixture. See Testcase Class per Fixture

reusing test code

reusing test code

symptoms

symptoms

Test Commands

Test Commands

Test Concerns Separately

Test Concerns Separately

test conditions

test conditions

test database

test database

test debt

test debt

Test Dependency in Production

Test Dependency in Production

Test Discovery

Test Discovery

introduction

introduction

Lost Tests solution

Lost Tests solution

pattern description

pattern description

Test Suite Object Generator

Test Suite Object Generator

Test Suite Objects

Test Suite Objects

Test Double

Test Double

Back Door Manipulation

Back Door Manipulation

Behavior Verification

Behavior Verification

Configurable Test Double. See Configurable Test Double

Configurable Test Double. See Configurable Test Double

configuring

configuring

considerations

considerations

customer testing

customer testing

database testing

database testing

Dependency Injection. See Dependency Injection

Dependency Injection. See Dependency Injection

Dependency Lookup

Dependency Lookup

dependency replacement

dependency replacement

design for testability

design for testability

Don't Modify the SUT

Don't Modify the SUT

Dummy Object

Dummy Object

example

example

Fake Object. See Fake Object

Fake Object. See Fake Object

Fragile Test

Fragile Test

Hard-Coded Test Double. See Hard-Coded Test Double

Hard-Coded Test Double. See Hard-Coded Test Double

Highly Coupled Code solution

Highly Coupled Code solution

indirect input and output

indirect input and output

indirect input control

indirect input control

indirect input, importance of

indirect input, importance of

indirect output, importance of

indirect output, importance of

indirect output verification

indirect output verification

installing

installing

minimizing risk

minimizing risk

Mock Object. See Mock Object

Mock Object. See Mock Object

other uses

other uses

outside-in development

outside-in development

overview

overview

providing

providing

retrofitting testability

retrofitting testability

reusing test code

reusing test code

terminology

terminology

vs. Test Hook

vs. Test Hook

Test Spy

Test Spy

Test Stub. See Test Stub

Test Stub. See Test Stub

Test-Specific Subclass. See Test-Specific Subclass

Test-Specific Subclass. See Test-Specific Subclass

types of

types of

when to use

when to use

Test Double Class

Test Double Class

example

example

implementation

implementation

Test Double Subclass

Test Double Subclass

implementation

implementation

when to use

when to use

test drivers

test drivers

Assertion Messages

Assertion Messages

defined

defined

test driving

test driving

Test Enumeration

Test Enumeration

introduction

introduction

pattern description

pattern description

test errors

test errors

test failure

test failure

test first development

test first development

defined

defined

process

process

test automation philosophy

test automation philosophy

vs. test-last development

vs. test-last development

Test Fixture Registry

Test Fixture Registry

accessing Shared Fixtures

accessing Shared Fixtures

Test Helper use

Test Helper use

test fixtures. See fixtures

test fixtures. See fixtures

Test Helper

Test Helper

Automated Teardown

Automated Teardown

introduction

introduction

pattern description

pattern description

Test Helper Mixin

Test Helper Mixin

example

example

vs. Testcase Superclass

vs. Testcase Superclass

Test Hook

Test Hook

pattern description

pattern description

in Procedural Test Stub

in Procedural Test Stub

retrofitting testability

retrofitting testability

Test Logic in Production

Test Logic in Production

testability

testability

Test Logic, Conditional. See Conditional Test Logic

Test Logic, Conditional. See Conditional Test Logic

Test Logic in Production

Test Logic in Production

Equality Pollution

Equality Pollution

impact

impact

introduction

introduction

symptoms

symptoms

Test Dependency in Production

Test Dependency in Production

Test Hooks

Test Hooks

For Tests Only

For Tests Only

test maintainer

test maintainer

Test Method

Test Method

calling Assertion. See Assertion Method

calling Assertion. See Assertion Method

Constructor Test example

Constructor Test example

Constructor Tests

Constructor Tests

Dependency Initialization Tests

Dependency Initialization Tests

enumeration

enumeration

Expected Exception Test

Expected Exception Test

Expected Exception Test using block closure

Expected Exception Test using block closure

Expected Exception Test using method attributes

Expected Exception Test using method attributes

Expected Exception Test using try/catch

Expected Exception Test using try/catch

fixture design

fixture design

implementation

implementation

invocation

invocation

Lost Tests

Lost Tests

minimizing untested code

minimizing untested code

organization. See also test organization patterns

organization. See also test organization patterns

overview

overview

persistent fixtures. See persistent fixtures

persistent fixtures. See persistent fixtures

right-sizing

right-sizing

running

running

selection

selection

Simple Success Test

Simple Success Test

Simple Success Test example

Simple Success Test example

test automation philosophies

test automation philosophies

Test Commands

Test Commands

Test Concerns Separately

Test Concerns Separately

Test Suite Objects

Test Suite Objects

Testcase Object implementation

Testcase Object implementation

transient fixture management. See transient fixtures

transient fixture management. See transient fixtures

unit testing

unit testing

Verify One Condition per Test

Verify One Condition per Test

writing simple tests

writing simple tests

Test Method Discovery

Test Method Discovery

defined

defined

examples

examples

Test Object Registry. See Automated Teardown

Test Object Registry. See Automated Teardown

test organization

test organization

code reuse

code reuse

introduction

introduction

naming conventions

naming conventions

overview

overview

right-sizing Test Methods

right-sizing Test Methods

test files

test files

Test Methods and Testcase Classes

Test Methods and Testcase Classes

test suites

test suites

test organization patterns

test organization patterns

Named Test Suite. See Named Test Suite

Named Test Suite. See Named Test Suite

Parameterized Test. See Parameterized Test

Parameterized Test. See Parameterized Test

Test Helper

Test Helper

Test Utility Method. See Test Utility Method

Test Utility Method. See Test Utility Method

Testcase Class per Class. See Testcase Class per Class

Testcase Class per Class. See Testcase Class per Class

Testcase Class per Feature. See Testcase Class per Feature

Testcase Class per Feature. See Testcase Class per Feature

Testcase Class per Fixture. See Testcase Class per Fixture

Testcase Class per Fixture. See Testcase Class per Fixture

Testcase Superclass

Testcase Superclass

test packages

test packages

defined

defined

test file organization

test file organization

test readers

test readers

test refactorings. See also refactoring

test refactorings. See also refactoring

Extractable Test Component

Extractable Test Component

In-line Resource

In-line Resource

Make Resources Unique

Make Resources Unique

Minimize Data

Minimize Data

Replace Dependency with Test Double

Replace Dependency with Test Double

Set Up External Resource

Set Up External Resource

test results

test results

defined

defined

introduction

introduction

verification. See result verification

verification. See result verification

Test Run War

Test Run War

database testing

database testing

Erratic Tests cause

Erratic Tests cause

introduction

introduction

vs. Shared Fixture strategy

vs. Shared Fixture strategy

Test Runner

Test Runner

Graphical. See Graphical Test Runner

Graphical. See Graphical Test Runner

implementation

implementation

introduction

introduction

Missing Assertion Messages

Missing Assertion Messages

overview

overview

Test Automation Frameworks

Test Automation Frameworks

test runs

test runs

Test Selection

Test Selection

pattern description

pattern description

Test Suite Object

Test Suite Object

test smells

test smells

aliases and causes

aliases and causes

behavior. See behavior smells

behavior. See behavior smells

catalog of

catalog of

code smells. See code smells

code smells. See code smells

database testing. See database testing

database testing. See database testing

defined

defined

introduction

introduction

overview

overview

patterns and principles vs.

patterns and principles vs.

project smells. See project smells

project smells. See project smells

reducing Test Code Duplication

reducing Test Code Duplication

Test Spy

Test Spy

Back Door Verification

Back Door Verification

Behavior Verification

Behavior Verification

Configurable. See Configurable Test Double

Configurable. See Configurable Test Double

examples

examples

implementation

implementation

indirect outputs verification

indirect outputs verification

introduction

introduction

motivating example

motivating example

overview

overview

Procedural Behavior Verification

Procedural Behavior Verification

refactoring

refactoring

when to use

when to use

xUnit terminology

xUnit terminology

test strategy patterns

test strategy patterns

Data-Driven Test. See Data-Driven Test

Data-Driven Test. See Data-Driven Test

Fresh Fixture. See Fresh Fixture

Fresh Fixture. See Fresh Fixture

Layer Test. See Layer Test

Layer Test. See Layer Test

Minimal Fixture

Minimal Fixture

Recorded Test. See Recorded Test

Recorded Test. See Recorded Test

Scripted Test

Scripted Test

Shared Fixture. See Shared Fixture

Shared Fixture. See Shared Fixture

Standard Fixture. See Standard Fixture

Standard Fixture. See Standard Fixture

Test Automation Framework

Test Automation Framework

test strippers

test strippers

Test Stub

Test Stub

Behavior-Modifying Subclass

Behavior-Modifying Subclass

Configurable. See Configurable Test Double

Configurable. See Configurable Test Double

configuring

configuring

Context Sensitivity solution

Context Sensitivity solution

controlling indirect inputs

controlling indirect inputs

creating in-line resources

creating in-line resources

examples

examples

implementation

implementation

indirect inputs control

indirect inputs control

inside-out development

inside-out development

introduction

introduction

motivating example

motivating example

overview

overview

refactoring

refactoring

unit testing

unit testing

when to use

when to use

xUnit terminology

xUnit terminology

test success

test success

Test Suite Enumeration

Test Suite Enumeration

defined

defined

example

example

Test Suite Factory

Test Suite Factory

Test Suite Object

Test Suite Object

enumeration

enumeration

Interacting Test Suites

Interacting Test Suites

introduction

introduction

pattern description

pattern description

Test Suite Object Generator

Test Suite Object Generator

Test Suite Object Simulator

Test Suite Object Simulator

Test Suite Procedure

Test Suite Procedure

defined

defined

example

example

test suites

test suites

defined

defined

Lost Tests

Lost Tests

Named Test Suites. See Named Test Suite

Named Test Suites. See Named Test Suite

Test Tree Explorer

Test Tree Explorer

Test Utility Method

Test Utility Method

Communicate Intent

Communicate Intent

eliminating loops

eliminating loops

example

example

implementation

implementation

introduction

introduction

motivating example

motivating example

Obscure Tests solution

Obscure Tests solution

overview

overview

reducing risk of bugs

reducing risk of bugs

refactoring

refactoring

reusing

reusing

reusing via Test Helper

reusing via Test Helper

reusing via Testcase Superclass

reusing via Testcase Superclass

using TDD to write

using TDD to write

when to use

when to use

Test Utility Test

Test Utility Test

testability, design for. See design-for-testability

testability, design for. See design-for-testability

Testcase Class

Testcase Class

introduction

introduction

organization

organization

pattern description

pattern description

reusable test logic

reusable test logic

selection

selection

Testcase Class Discovery

Testcase Class Discovery

defined

defined

example

example

Testcase Class per Class

Testcase Class per Class

example

example

implementation

implementation

overview

overview

when to use

when to use

Testcase Class per Feature

Testcase Class per Feature

example

example

implementation

implementation

motivating example

motivating example

overview

overview

refactoring

refactoring

when to use

when to use

Testcase Class per Fixture

Testcase Class per Fixture

example

example

implementation

implementation

motivating example

motivating example

overview

overview

refactoring

refactoring

Verify One Condition per Test

Verify One Condition per Test

when to use

when to use

Testcase Class per Method

Testcase Class per Method

Testcase Class per User Story

Testcase Class per User Story

Testcase Object

Testcase Object

introduction

introduction

pattern description

pattern description

Testcase Superclass

Testcase Superclass

pattern description

pattern description

reusing test code

reusing test code

Test Discovery using

Test Discovery using

test-driven bug fixing

test-driven bug fixing

test-driven development (TDD). See TDD (test-driven development)

test-driven development (TDD). See TDD (test-driven development)

Test-Driven Development: By Example (Beck)

Test-Driven Development: By Example (Beck)

test-driven testability

test-driven testability

Testing by Layers. See Layer Test

Testing by Layers. See Layer Test

testing terminology. See terminology

testing terminology. See terminology

test-last development

test-last development

defined

defined

strategy

strategy

test automation philosophy

test automation philosophy

vs. test-first development

vs. test-first development

TestNG

TestNG

defined

defined

Interacting Tests

Interacting Tests

Testcase Object exception

Testcase Object exception

vs. xUnit

vs. xUnit

Tests as Documentation

Tests as Documentation

Communicate Intent

Communicate Intent

customer testing

customer testing

defined

defined

reusing test code

reusing test code

unit testing

unit testing

Tests as Safety Net

Tests as Safety Net

Tests as Specification

Tests as Specification

test-specific equality

test-specific equality

Test-Specific Extension. See Test-Specific Subclass

Test-Specific Extension. See Test-Specific Subclass

Test-Specific Subclass

Test-Specific Subclass

Behavior-Exposing Subclass

Behavior-Exposing Subclass

Behavior-Modifying Subclass (Substituted Singleton)

Behavior-Modifying Subclass (Substituted Singleton)

Behavior-Modifying Subclass (Test Stub)

Behavior-Modifying Subclass (Test Stub)

defining Test-Specific Equality

defining Test-Specific Equality

Don't Modify the SUT

Don't Modify the SUT

implementation

implementation

Isolate the SUT

Isolate the SUT

motivating example

motivating example

overview

overview

refactoring

refactoring

retrofitting testability

retrofitting testability

State-Exposing Subclass

State-Exposing Subclass

For Tests Only solution

For Tests Only solution

when to use

when to use

Test::Unit

Test::Unit

Thread-Specific Storage

Thread-Specific Storage

Too Many Tests

Too Many Tests

tools

tools

automated unit testing

automated unit testing

commercial record and playback

commercial record and playback

QTP. See QTP (QuickTest Professional)

QTP. See QTP (QuickTest Professional)

robot user. See robot user tools

robot user. See robot user tools

for test automation strategy

for test automation strategy

types of

types of

Transaction Controller, Humble. See Humble Transaction Controller

Transaction Controller, Humble. See Humble Transaction Controller

Transaction Rollback Teardown

Transaction Rollback Teardown

data access layer testing

data access layer testing

defined

defined

examples

examples

implementation

implementation

motivating example

motivating example

overview

overview

refactoring

refactoring

when to use

when to use

transient fixtures

transient fixtures

Delegated Setup

Delegated Setup

hybrid setup

hybrid setup

Implicit Setup

Implicit Setup

In-Line Setup

In-Line Setup

overview

overview

vs. persistent fixtures

vs. persistent fixtures

tearing down

tearing down

terminology

terminology

what's next

what's next

Transient Fresh Fixture

Transient Fresh Fixture

database testing

database testing

defined

defined

vs. Shared Fixture

vs. Shared Fixture

troubleshooting

troubleshooting

Buggy Tests

Buggy Tests

Developers Not Writing Tests

Developers Not Writing Tests

Erratic Tests

Erratic Tests

Fragile Tests

Fragile Tests

High Test Maintenance Cost

High Test Maintenance Cost

Slow Tests

Slow Tests

True Humble Executable

True Humble Executable

True Humble Objects

True Humble Objects

TRUNCATE command. See Table Truncation Teardown

TRUNCATE command. See Table Truncation Teardown

try/catch

try/catch

Expected Exception Tests

Expected Exception Tests

Single-Outcome Assertions

Single-Outcome Assertions

try/finally block

try/finally block

cleaning up fixture teardown logic

cleaning up fixture teardown logic

Implicit Teardown

Implicit Teardown

In-line Teardown

In-line Teardown

type compatibility

type compatibility

type visibility

type visibility

Test Helper use

Test Helper use

Test Utility Methods

Test Utility Methods

Testcase Superclass use

Testcase Superclass use

U

U

UAT (user acceptance tests)

UAT (user acceptance tests)

defined

defined

principles

principles

UI (User Interface) tests

UI (User Interface) tests

asynchronous tests

asynchronous tests

Hard-To-Test Code

Hard-To-Test Code

tools

tools

UML (Unified Modeling Language)

UML (Unified Modeling Language)

Unconfigurable Test Doubles

Unconfigurable Test Doubles

unexpected exceptions

unexpected exceptions

Unfinished Test Assertion

Unfinished Test Assertion

Unfinished Test Method from Template

Unfinished Test Method from Template

Unified Modeling Language (UML)

Unified Modeling Language (UML)

unique resources

unique resources

Unit Testing with Java (Link)

Unit Testing with Java (Link)

unit tests

unit tests

defined

defined

introduction

introduction

per-functionality

per-functionality

rules

rules

Scripted Tests

Scripted Tests

xUnit vs. Fit

xUnit vs. Fit

unnecessary object elimination

unnecessary object elimination

Unrepeatable Test

Unrepeatable Test

database testing

database testing

Erratic Test cause

Erratic Test cause

introduction

introduction

persistent fresh fixtures

persistent fresh fixtures

vs. Repeatable Test

vs. Repeatable Test

Untestable Test Code

Untestable Test Code

avoiding Conditional Logic

avoiding Conditional Logic

Hard-To-Test Code

Hard-To-Test Code

Untested Code

Untested Code

alternative path verification

alternative path verification

indirect inputs and

indirect inputs and

Isolate the SUT

Isolate the SUT

minimizing

minimizing

preventing with Test Doubles

preventing with Test Doubles

Production Bugs

Production Bugs

unit testing

unit testing

Untested Requirement

Untested Requirement

Frequent Debugging cause

Frequent Debugging cause

indirect output testing

indirect output testing

preventing with Test Doubles

preventing with Test Doubles

Production Bugs cause

Production Bugs cause

reducing via Isolate the SUT

reducing via Isolate the SUT

usability tests

usability tests

use cases

use cases

Use the Front Door First

Use the Front Door First

defined

defined

Overspecified Software avoidance

Overspecified Software avoidance

user acceptance tests (UAT)

user acceptance tests (UAT)

defined

defined

principles

principles

User Interface (UI) tests

User Interface (UI) tests

asynchronous tests

asynchronous tests

Hard-To-Test Code

Hard-To-Test Code

tools

tools

user story

user story

defined

defined

Testcase Class per

Testcase Class per

utility methods. See Test Utility Method

utility methods. See Test Utility Method

utPLSQL

utPLSQL

V

V

value patterns

value patterns

Derived Values

Derived Values

Dummy Objects

Dummy Objects

Generated Values

Generated Values

Literal Values

Literal Values

variables

variables

in Derived Values

in Derived Values

global

global

instance. See instance variables

instance. See instance variables

local. See local variables

local. See local variables

procedure variables

procedure variables

static

static

VB Lite Unit

VB Lite Unit

VbUnit

VbUnit

defined

defined

Suite Fixture Setup support

Suite Fixture Setup support

Testcase Class terminology

Testcase Class terminology

xUnit terminology

xUnit terminology

Verbose Tests. See Obscure Test

Verbose Tests. See Obscure Test

verification

verification

alternative path

alternative path

Back Door Manipulation

Back Door Manipulation

Back Door using Test Spy

Back Door using Test Spy

cleaning up logic

cleaning up logic

direct output

direct output

indirect outputs

indirect outputs

state vs. behavior

state vs. behavior

test results. See result verification

test results. See result verification

Verify One Condition per Test

Verify One Condition per Test

Verification Method

Verification Method

defined

defined

example

example

Verify One Condition per Test

Verify One Condition per Test

defined

defined

right-sizing Test Methods

right-sizing Test Methods

verify outcome

verify outcome

Virtual Clock

Virtual Clock

visibility

visibility

of SUT features from Test-Specific Subclass

of SUT features from Test-Specific Subclass

test file organization

test file organization

type. See type visibility

type. See type visibility

visual objects, Humble Dialog use

visual objects, Humble Dialog use

Visual Studio

Visual Studio

W

W

waterfall design

waterfall design

Watir

Watir

defined

defined

Test Automation Frameworks

Test Automation Frameworks

test automation tools

test automation tools

Weinberg, Gerry

Weinberg, Gerry

widgets

widgets

Humble Dialog use

Humble Dialog use

recognizers

recognizers

Wikipedia

Wikipedia

Working Effectively with Legacy Code (Feathers)

Working Effectively with Legacy Code (Feathers)

Write the Tests First

Write the Tests First

writing tests

writing tests

Developers Not Writing Tests project smells

Developers Not Writing Tests project smells

development process

development process

goals

goals

philosophies. See test automation philosophies

philosophies. See test automation philosophies

principles. See test automation principles

principles. See test automation principles

X

X

XML data files, Data-Driven Tests

XML data files, Data-Driven Tests

xUnit

xUnit

Data-Driven Tests with CSV input file

Data-Driven Tests with CSV input file

Data-Driven Tests with XML data file

Data-Driven Tests with XML data file

defined

defined

family members

family members

vs. Fit

vs. Fit

fixture definitions

fixture definitions

Interacting Test Suites

Interacting Test Suites

introduction

introduction

language-specific terminology

language-specific terminology

modern

modern

Naive xUnit Test Interpreter

Naive xUnit Test Interpreter

profiling tools

profiling tools

Suite Fixture Setup support

Suite Fixture Setup support

sweet spot

sweet spot

terminology

terminology

Test Automation Frameworks

Test Automation Frameworks

test fixtures

test fixtures

test organization mechanisms

test organization mechanisms

xUnit basics

xUnit basics

defining suites of tests

defining suites of tests

defining tests

defining tests

fixtures

fixtures

overview

overview

procedural world

procedural world

running Test Methods

running Test Methods

running tests

running tests

Test Commands

Test Commands

test results

test results

Test Suite Object

Test Suite Object

xUnit basics patterns

xUnit basics patterns

Assertion Message

Assertion Message

Assertion Method. See Assertion Method

Assertion Method. See Assertion Method

Four-Phase Test

Four-Phase Test

Test Discovery

Test Discovery

Test Enumeration

Test Enumeration

Test Method. See Test Method

Test Method. See Test Method

Test Runner. See Test Runner

Test Runner. See Test Runner

Test Selection

Test Selection

Test Suite Object

Test Suite Object

Testcase Class

Testcase Class

Testcase Object

Testcase Object

Footnotes

Footnotes

 

Preface

Preface

1 The Pattern Languages of Programs conference.

1 The Pattern Languages of Programs conference.

2 Technically, they are not truly patterns until they have been discovered by three independent project teams.

2 Technically, they are not truly patterns until they have been discovered by three independent project teams.

3 The testing function is sometimes referred to as "Quality Assurance." This usage is, strictly speaking, incorrect.

3 The testing function is sometimes referred to as "Quality Assurance." This usage is, strictly speaking, incorrect.

4 The Law of Raspberry Jam: "The wider you spread it, the thinner it gets."

4 The Law of Raspberry Jam: "The wider you spread it, the thinner it gets."

Introduction

Introduction

1 A small percentage of the unit tests may correspond directly to the business logic described in the requirements and the customer tests, but a large majority tests the code that surrounds the business logic.

1 A small percentage of the unit tests may correspond directly to the business logic described in the requirements and the customer tests, but a large majority tests the code that surrounds the business logic.

2 A change in behavior could occur because the system is doing something different or because it is doing the same thing with different timing or sequencing.

2 A change in behavior could occur because the system is doing something different or because it is doing the same thing with different timing or sequencing.

3 Some might want to call these patterns "anti-patterns." Just because a pattern often has negative consequences, it does not imply that the pattern is always bad. For this reason, I prefer not to call these anti-patterns; I just do not use them very often.

3 Some might want to call these patterns "anti-patterns." Just because a pattern often has negative consequences, it does not imply that the pattern is always bad. For this reason, I prefer not to call these anti-patterns; I just do not use them very often.

4 In a few cases, there are even a pattern and a smell with similar names.

4 In a few cases, there are even a pattern and a smell with similar names.

5 The "sniff test" is based on the diaper story in [Ref] wherein Kent Beck asks Grandma Beck, "How do I know that it is time to change the diaper?" "If it stinks, change it!" was her response. Smells are named based on the "stink," not the cause of the stink.

5 The "sniff test" is based on the diaper story in [Ref] wherein Kent Beck asks Grandma Beck, "How do I know that it is time to change the diaper?" "If it stinks, change it!" was her response. Smells are named based on the "stink," not the cause of the stink.

Refactoring a Test

Refactoring a Test

1 While the need to wrap lines to keep them at 65 characters makes this code look even longer than it really is, it is still unnecessarily long. It contains 25 executable statements including initialized declarations, 6 lines of control statements, 4 in-line comments, and 2 lines to declare the test method—giving a total of 37 lines of unwrapped source code.

1 While the need to wrap lines to keep them at 65 characters makes this code look even longer than it really is, it is still unnecessarily long. It contains 25 executable statements including initialized declarations, 6 lines of control statements, 4 in-line comments, and 2 lines to declare the test method—giving a total of 37 lines of unwrapped source code.

2 It's a good thing we are not being rewarded for the number of lines of code we write! This is yet another example of why KLOC is such a poor measure of productivity.

2 It's a good thing we are not being rewarded for the number of lines of code we write! This is yet another example of why KLOC is such a poor measure of productivity.

3 The test reader cannot see the objects being used by the test.

3 The test reader cannot see the objects being used by the test.

4 Ignoring wrapped lines, we have 6 executable statements surrounded by the two lines of method declarations/end.

4 Ignoring wrapped lines, we have 6 executable statements surrounded by the two lines of method declarations/end.

Chapter 1

Chapter 1

1 If our customer cannot define the tests before we have built the software, we have every reason to be worried!

1 If our customer cannot define the tests before we have built the software, we have every reason to be worried!

2 We will likely find fewer Missing Unit Tests (see Production Bugs on page 268) when we practice test-driven development than if we adopt a "test last" policy. Even so, there is still value in running the code coverage tools with TDD.

2 We will likely find fewer Missing Unit Tests (see Production Bugs on page 268) when we practice test-driven development than if we adopt a "test last" policy. Even so, there is still value in running the code coverage tools with TDD.

Chapter 2

Chapter 2

1 This practice is also called "root cause analysis" or "peeling the onion" in some circles.

1 This practice is also called "root cause analysis" or "peeling the onion" in some circles.

2 It can be hard enough to get project managers to buy into letting developers write automated tests. It is crucial that we don't squander this opportunity by being sloppy or inefficient. The need for this balancing act is, in a nutshell, why I started writing this book: to help developers succeed and avoid giving the pessimistic project manager an excuse for calling a halt to automated unit testing.

2 It can be hard enough to get project managers to buy into letting developers write automated tests. It is crucial that we don't squander this opportunity by being sloppy or inefficient. The need for this balancing act is, in a nutshell, why I started writing this book: to help developers succeed and avoid giving the pessimistic project manager an excuse for calling a halt to automated unit testing.

3 Note that I said "reuse test logic" and not "reuse Test Methods."

3 Note that I said "reuse test logic" and not "reuse Test Methods."

4 It is equally important that we do not reuse Test Methods, as that practice results in Flexible Tests (see Conditional Test Logic).

4 It is equally important that we do not reuse Test Methods, as that practice results in Flexible Tests (see Conditional Test Logic).

5 See the sidebar on Ariane (page 218) for a cautionary tale.

5 See the sidebar on Ariane (page 218) for a cautionary tale.

Chapter 3

Chapter 3

1 The argument that the quality improvement is worth the extra cost also doesn't go very far in these days of "just good enough" software quality.

1 The argument that the quality improvement is worth the extra cost also doesn't go very far in these days of "just good enough" software quality.

2 Imagine trying to learn to be a trapeze artist in the circus without having that big net that allows you to make mistakes. You would never progress beyond swinging back and forth!

2 Imagine trying to learn to be a trapeze artist in the circus without having that big net that allows you to make mistakes. You would never progress beyond swinging back and forth!

3 "With less paranoia" is probably more accurate!

3 "With less paranoia" is probably more accurate!

4 There should be at least one Test Method for each unique path through the code; often there will be several, one for each boundary value of the equivalence class.

4 There should be at least one Test Method for each unique path through the code; often there will be several, one for each boundary value of the equivalence class.

Chapter 4

Chapter 4

1 For example, high-quality software, fit for purpose, on time, under budget.

1 For example, high-quality software, fit for purpose, on time, under budget.

Chapter 5

Chapter 5

1 Anything more than about ten lines is getting to be too much.

1 Anything more than about ten lines is getting to be too much.

2 Clever testers often use automated test scripts to put the SUT into the correct starting state for their manual tests, thereby avoiding long manual test scripts.

2 Clever testers often use automated test scripts to put the SUT into the correct starting state for their manual tests, thereby avoiding long manual test scripts.

3 For example, AwaitingApprovalFlight.validApproverRequestShouldBeApproved.

3 For example, AwaitingApprovalFlight.validApproverRequestShouldBeApproved.

Chapter 6

Chapter 6

1 Most of the tools in this quadrant focus on recording regression tests by inserting observation points into a component-based application and recording the (remote) method calls and responses between the components. This approach is becoming more popular with the advent of service-oriented architecture (SOA).

1 Most of the tools in this quadrant focus on recording regression tests by inserting observation points into a component-based application and recording the (remote) method calls and responses between the components. This approach is becoming more popular with the advent of service-oriented architecture (SOA).

2 The name is derived from what directors in Hollywood tell aspiring applicants at mass casting calls: "Don't call us; we'll call you (if we want you)."

2 The name is derived from what directors in Hollywood tell aspiring applicants at mass casting calls: "Don't call us; we'll call you (if we want you)."

3 Technically, SUnit came first but it took JUnit and the "Test Infected" article [TI] to really get things rolling.

3 Technically, SUnit came first but it took JUnit and the "Test Infected" article [TI] to really get things rolling.

4 Unfortunately, this may result in slower tests when the fixture is in a database. Nevertheless, it will still be many times faster than if each test had to insert all the records it needed.

4 Unfortunately, this may result in slower tests when the fixture is in a database. Nevertheless, it will still be many times faster than if each test had to insert all the records it needed.

5 "Big Design Upfront" (also known as "waterfall design") is the opposite of emergent design ("just-in-time design").

5 "Big Design Upfront" (also known as "waterfall design") is the opposite of emergent design ("just-in-time design").

6 I am deliberately not saying "SUT" here because it interacts with more than just the SUT.

6 I am deliberately not saying "SUT" here because it interacts with more than just the SUT.

7 An asynchronous observation point.

7 An asynchronous observation point.

8 A synchronous observation point.

8 A synchronous observation point.

9 These typically take the form of if  (testing)  then  ...  else  ...  endif.

9 These typically take the form of if  (testing)  then  ...  else  ...  endif.

10 This Test Double can be either hard-coded or file driven. Either way, it should be independent of the real implementation so that the UI tests need to know only which data to use to evoke specific behaviors from the Service Facade, not the logic behind it.

10 This Test Double can be either hard-coded or file driven. Either way, it should be independent of the real implementation so that the UI tests need to know only which data to use to evoke specific behaviors from the Service Facade, not the logic behind it.

11 Any UI that contains state information or supports conditional display or enabling of elements should be considered nontrivial.

11 Any UI that contains state information or supports conditional display or enabling of elements should be considered nontrivial.

Chapter 7

Chapter 7

1 See the sidebar "Testing Stored Procs with JUnit" (page 657) for an example of using a testing framework in one language to test an SUT in another language.

1 See the sidebar "Testing Stored Procs with JUnit" (page 657) for an example of using a testing framework in one language to test an SUT in another language.

2 Even those xUnit variants that don't have an explicit Suite class or method still build Test Suite Objects behind the scene.

2 Even those xUnit variants that don't have an explicit Suite class or method still build Test Suite Objects behind the scene.

3 This scheme is called a test fixture in some variants of xUnit, probably because the creators assumed we would have a single Testcase Class per Fixture (page 631).

3 This scheme is called a test fixture in some variants of xUnit, probably because the creators assumed we would have a single Testcase Class per Fixture (page 631).

4 For example, before executing an assertion on the contents of a field of an object returned by the SUT, it is worthwhile to assertNotNull on the object reference so as to avoid a "null reference" error.

4 For example, before executing an assertion on the contents of a field of an object returned by the SUT, it is worthwhile to assertNotNull on the object reference so as to avoid a "null reference" error.

5 NUnit is a known exception and others may exist. See the sidebar "There's Always an Exception" (page 384) for more information.

5 NUnit is a known exception and others may exist. See the sidebar "There's Always an Exception" (page 384) for more information.

Chapter 8

Chapter 8

1 See the sidebar "There's Always an Exception" (page 384).

1 See the sidebar "There's Always an Exception" (page 384).

2 This discussion assumes that the SUT is an object and not just static methods on a class.

2 This discussion assumes that the SUT is an object and not just static methods on a class.

3 When referenced via a Test Helper (page 643) class, they are often called the Object Mother pattern (see Test Helper on page 643).

3 When referenced via a Test Helper (page 643) class, they are often called the Object Mother pattern (see Test Helper on page 643).

Chapter 9

Chapter 9

1 This is less of an issue with Testcase Class per Fixture (page 631) because the fixture should always be the same. With other Testcase Class organizations, we may need to include Teardown Guard Clauses (see In-line Teardown) within the tearDown method to ensure that it doesn't produce errors when it runs.

1 This is less of an issue with Testcase Class per Fixture (page 631) because the fixture should always be the same. With other Testcase Class organizations, we may need to include Teardown Guard Clauses (see In-line Teardown) within the tearDown method to ensure that it doesn't produce errors when it runs.

2 Unlike setup code, which is often very important for understanding the test.

2 Unlike setup code, which is often very important for understanding the test.

3 This is two orders of magnitude!

3 This is two orders of magnitude!

4 Your mileage may vary.

4 Your mileage may vary.

5 Think of it as a built-in decorator for a single Testcase Class.

5 Think of it as a built-in decorator for a single Testcase Class.

Chapter 10

Chapter 10

1 The one exception is when we must use a Shared Fixture (page 317); it may be worthwhile to use a Guard Assertion (page 490) to document what the test requires from it and to produce a test failure if the fixture is corrupted. We could also do so from within the Finder Methods (see Test Utility Method) that we use to retrieve the objects in the Shared Fixture (page 317) we will use in our tests.

1 The one exception is when we must use a Shared Fixture (page 317); it may be worthwhile to use a Guard Assertion (page 490) to document what the test requires from it and to produce a test failure if the fixture is corrupted. We could also do so from within the Finder Methods (see Test Utility Method) that we use to retrieve the objects in the Shared Fixture (page 317) we will use in our tests.

2 In his book [TDD-APG], Dave Astels claims he never/rarely used the Eclipse Debugger while writing the code samples because the assertions always told him enough about what was wrong. This is what we strive for!

2 In his book [TDD-APG], Dave Astels claims he never/rarely used the Eclipse Debugger while writing the code samples because the assertions always told him enough about what was wrong. This is what we strive for!

3 A Test Spy built into the Testcase Class (page 373).

3 A Test Spy built into the Testcase Class (page 373).

4 We should always give this method an Intent-Revealing Name and stub it out with a call to the fail assertion to remind ourselves that we still need to write the method's body.

4 We should always give this method an Intent-Revealing Name and stub it out with a call to the fail assertion to remind ourselves that we still need to write the method's body.

Chapter 11

Chapter 11

1 Also called function pointers.

1 Also called function pointers.

2 Lenient Mock Objects are sometimes called "nice," but "lenient" is a more precise adjective.

2 Lenient Mock Objects are sometimes called "nice," but "lenient" is a more precise adjective.

3 JMock and its ports to other languages are good examples of such toolkits. Other toolkits, such as EasyMock, implement Statically Generated Test Doubles (see Configurable Test Double) by generating code that is then compiled just like a Hand-Built Test Double.

3 JMock and its ports to other languages are good examples of such toolkits. Other toolkits, such as EasyMock, implement Statically Generated Test Doubles (see Configurable Test Double) by generating code that is then compiled just like a Hand-Built Test Double.

4 A Dummy Object can be used as an observation point to verify that it was never used by ensuring that the Dummy Object throws an exception if any of its methods are called.

4 A Dummy Object can be used as an observation point to verify that it was never used by ensuring that the Dummy Object throws an exception if any of its methods are called.

5 This approach was advocated in the original paper on Mock Objects [ET]. In this paper, Mock Objects passed as parameters to methods are called "Smart Handlers."

5 This approach was advocated in the original paper on Mock Objects [ET]. In this paper, Mock Objects passed as parameters to methods are called "Smart Handlers."

6 "When you have a new hammer, everything looks like a nail."

6 "When you have a new hammer, everything looks like a nail."

Chapter 12

Chapter 12

1 A Test Method that contains Conditional Test Logic (page 200) is a sign of a test trying to accommodate different circumstances because it does not have control of all indirect inputs of the SUT or because it is trying to verify complex expected states on an in-line basis within the Test Method.

1 A Test Method that contains Conditional Test Logic (page 200) is a sign of a test trying to accommodate different circumstances because it does not have control of all indirect inputs of the SUT or because it is trying to verify complex expected states on an in-line basis within the Test Method.

2 He calls it "Just for Laughs" but I don't find that name very intent-revealing.

2 He calls it "Just for Laughs" but I don't find that name very intent-revealing.

3 Many xUnit variants "encourage" us to start all our Test Method names with "test" so that these methods can be automatically detected and added to the Test Suite Object. This constrains our naming somewhat compared to variants that indicate test methods via method attributes or annotations.

3 Many xUnit variants "encourage" us to start all our Test Method names with "test" so that these methods can be automatically detected and added to the Test Suite Object. This constrains our naming somewhat compared to variants that indicate test methods via method attributes or annotations.

4 I usually call it MyTest.

4 I usually call it MyTest.

5 Java offers another way to get around the visibility issue: We can define our own test Security  Manager to allow tests to access all methods on the SUT, not just the "package-protected" ones. This approach solves the problem in a general way but requires a good understanding of Java class loaders. Other languages may not have the equivalent functionality (or problem!).

5 Java offers another way to get around the visibility issue: We can define our own test Security  Manager to allow tests to access all methods on the SUT, not just the "package-protected" ones. This approach solves the problem in a general way but requires a good understanding of Java class loaders. Other languages may not have the equivalent functionality (or problem!).

Chapter 13

Chapter 13

1 Just one more example of how design for testability improves the design of our applications.

1 Just one more example of how design for testability improves the design of our applications.

2 Can you image asking a team of carpenters to share a single hammer?

2 Can you image asking a team of carpenters to share a single hammer?

3 For a more complete treatment of the topic, refer to [RDb].

3 For a more complete treatment of the topic, refer to [RDb].

Chapter 14

Chapter 14

1 Although it can also be used that way, I find it better to write the assertions first and then work back from there.

1 Although it can also be used that way, I find it better to write the assertions first and then work back from there.

2 See Robust Test (see page 29) and Repeatable Test (see page 26) for a more detailed description.

2 See Robust Test (see page 29) and Repeatable Test (see page 26) for a more detailed description.

Chapter 15

Chapter 15

1 See In-line Resource (page 736) refactoring for details.

1 See In-line Resource (page 736) refactoring for details.

2 Switching to an Immutable Shared Fixture (see Shared Fixture) does not fully address the core of this problem because it does not help us determine which parts of the fixture are needed by each test; only the parts that are modified are so identified!

2 Switching to an Immutable Shared Fixture (see Shared Fixture) does not fully address the core of this problem because it does not help us determine which parts of the fixture are needed by each test; only the parts that are modified are so identified!

3 We would also like to recoup this cost by reducing effort somewhere else. The best way to achieve this is to avoid Frequent Debugging (page 248) by writing the tests first and achieving Defect Localization (see page 22).

3 We would also like to recoup this cost by reducing effort somewhere else. The best way to achieve this is to avoid Frequent Debugging (page 248) by writing the tests first and achieving Defect Localization (see page 22).

Chapter 16

Chapter 16

1 Other tests may fail because we have removed the code that made them pass—but at least we have established which part of the code they depend on.

1 Other tests may fail because we have removed the code that made them pass—but at least we have established which part of the code they depend on.

2 Often called "screen scraping."

2 Often called "screen scraping."

Chapter 17

Chapter 17

1 For example, NUnit lets us put the attribute [Ignore] on a Test Method to keep it from being run.

1 For example, NUnit lets us put the attribute [Ignore] on a Test Method to keep it from being run.

Chapter 18

Chapter 18

1 The name "record, refactor, playback" was coined by Adam Geras.

1 The name "record, refactor, playback" was coined by Adam Geras.

2 BPT is short for "Business Process Testing."

2 BPT is short for "Business Process Testing."

3 Among test drivers, a legacy application is any system that lacks a safety net of automated tests.

3 Among test drivers, a legacy application is any system that lacks a safety net of automated tests.

4 Of course, we should be managing our test data in a version-controlled Repository, too—but that topic could fill another book; see [RDb] for details.

4 Of course, we should be managing our test data in a version-controlled Repository, too—but that topic could fill another book; see [RDb] for details.

5 The tabular data must be injected into the SUT during the fixture setup or exercise SUT phases or retrieved from the SUT during the result verification phase.

5 The tabular data must be injected into the SUT during the fixture setup or exercise SUT phases or retrieved from the SUT during the result verification phase.

6 This is very similar to how xUnit's built-in Test Method Discovery (see Test Discovery) mechanism works, except that we are passing in the test data in addition to the Test Method name.

6 This is very similar to how xUnit's built-in Test Method Discovery (see Test Discovery) mechanism works, except that we are passing in the test data in addition to the Test Method name.

7 Doing it with In-line Setup (page 408) would be silly—we would have to copy the code to construct the Standard Fixture to every Test Method.

7 Doing it with In-line Setup (page 408) would be silly—we would have to copy the code to construct the Standard Fixture to every Test Method.

8 Most members of the xUnit family create a separate Testcase Object (page 382) for each Test Method. A few do not, however. This difference can trip up unwary test automaters when they first start using these members of the family because instance variables may unexpectedly act like class variables. For a detailed description of this issue, see the sidebar "There's Always an Exception" (page 384).

8 Most members of the xUnit family create a separate Testcase Object (page 382) for each Test Method. A few do not, however. This difference can trip up unwary test automaters when they first start using these members of the family because instance variables may unexpectedly act like class variables. For a detailed description of this issue, see the sidebar "There's Always an Exception" (page 384).

9 The sidebar "Unit Test Rulz" (page 307) explains what constitutes a unit test.

9 The sidebar "Unit Test Rulz" (page 307) explains what constitutes a unit test.

10 It may not be as simple as looking at the first test that failed.

10 It may not be as simple as looking at the first test that failed.

11 Not all presentation logic relates to the user interface; this logic can also appear in a messaging interface used by another application.

11 Not all presentation logic relates to the user interface; this logic can also appear in a messaging interface used by another application.

12 Less likely than a test that exercises the logic via the presentation layer, that is.

12 Less likely than a test that exercises the logic via the presentation layer, that is.

13 Typically this data goes directly into a shared database or is injected via a "data pump."

13 Typically this data goes directly into a shared database or is injected via a "data pump."

14 I'm glossing over the various error-handling tests to simplify this discussion, but note that a Layer Test also makes it easier to exercise the error-handling logic.

14 I'm glossing over the various error-handling tests to simplify this discussion, but note that a Layer Test also makes it easier to exercise the error-handling logic.

Chapter 19

Chapter 19

1 See the sidebar "There's Always an Exception" (page 384) for an explanation of when this isn't the case.

1 See the sidebar "There's Always an Exception" (page 384) for an explanation of when this isn't the case.

2 This approach is particularly useful when we are building Mock Objects (page 544) because these objects are outside the Testcase Class but need to invoke Assertion Methods.

2 This approach is particularly useful when we are building Mock Objects (page 544) because these objects are outside the Testcase Class but need to invoke Assertion Methods.

3 These are the pre-conditions of the test.

3 These are the pre-conditions of the test.

4 I replaced part of the name with ".." to keep each line within the page width limit.

4 I replaced part of the name with ".." to keep each line within the page width limit.

5 For those who might be wondering what happened to the verify outcome phase of the test, there isn't one in this test. It is neither a Self-Checking Test nor a Single-Condition Test. Shame on me!

5 For those who might be wondering what happened to the verify outcome phase of the test, there isn't one in this test. It is neither a Self-Checking Test nor a Single-Condition Test. Shame on me!

6 A Smoke Test [SCM] suite is a good example.

6 A Smoke Test [SCM] suite is a good example.

7 A Smoke Test [SCM] suite is a good example.

7 A Smoke Test [SCM] suite is a good example.

Chapter 20

Chapter 20

1 Of course, there are other ways to set up the Shared Fixture, such as Setup Decorator and Suite Fixture Setup.

1 Of course, there are other ways to set up the Shared Fixture, such as Setup Decorator and Suite Fixture Setup.

2 If we don't have a refactoring tool handy, no worries. Just end the Test Method after each subtest and type in the signature of the next Test Method before the next subtest. We then move any Shared Fixture variables out of the first Test Method.

2 If we don't have a refactoring tool handy, no worries. Just end the Test Method after each subtest and type in the signature of the next Test Method before the next subtest. We then move any Shared Fixture variables out of the first Test Method.

Chapter 21

Chapter 21

1 The natural example for this pattern is not very good at illustrating the difference between State Verification and Behavior Verification. For this purpose, refer to Behavior Verification, which provides a second example of State Verification that is more directly comparable.

1 The natural example for this pattern is not very good at illustrating the difference between State Verification and Behavior Verification. For this purpose, refer to Behavior Verification, which provides a second example of State Verification that is more directly comparable.

Chapter 22

Chapter 22

1 That is, they augment the Implicit Teardown with some additional In-line Setup (page 408) or Delegated Setup (page 411).

1 That is, they augment the Implicit Teardown with some additional In-line Setup (page 408) or Delegated Setup (page 411).

Chapter 23

Chapter 23

1 My mother grew up in Hungary and has retained a part of her Hungarian accent—think Zsa Zsa Gabor—all her life. She says, "It is important to put the emphasis on the right syllable."

1 My mother grew up in Hungary and has retained a part of her Hungarian accent—think Zsa Zsa Gabor—all her life. She says, "It is important to put the emphasis on the right syllable."

2 Technically, the SUT is whatever software we are testing and doesn't include anything it depends on; thus "inside" is somewhat of a misnomer. It is better to think of the DOC that is the destination of the indirect outputs as being "behind" the SUT and part of the fixture.

2 Technically, the SUT is whatever software we are testing and doesn't include anything it depends on; thus "inside" is somewhat of a misnomer. It is better to think of the DOC that is the destination of the indirect outputs as being "behind" the SUT and part of the fixture.

3 This usually requires that we subclass our testcase from a special MockObjectTestCase class.

3 This usually requires that we subclass our testcase from a special MockObjectTestCase class.

4 We rarely move from a Configurable Test Double to a Hard-Coded Test Double because we generally seek to make the Test Double more—not less—reusable.

4 We rarely move from a Configurable Test Double to a Hard-Coded Test Double because we generally seek to make the Test Double more—not less—reusable.

5 That is, by using a concrete class as the type of the variable rather than an abstract class or interface.

5 That is, by using a concrete class as the type of the variable rather than an abstract class or interface.

6 This decision is enabled by the fact that getTime was defined to be protected; we would not be able to do this if it was private.

6 This decision is enabled by the fact that getTime was defined to be protected; we would not be able to do this if it was private.

7 We could have used a Hard-Coded Test Double (page 568) subclass instead, but that tactic would have required a different Test-Specific Subclass for each time we want to test with. Each subclass would simply hard-code the return value of the getTime method.

7 We could have used a Hard-Coded Test Double (page 568) subclass instead, but that tactic would have required a different Test-Specific Subclass for each time we want to test with. Each subclass would simply hard-code the return value of the getTime method.

8 A private method cannot be seen or overridden by a subclass.

8 A private method cannot be seen or overridden by a subclass.

9 This choice prevents a subclass from overriding the method's behavior.

9 This choice prevents a subclass from overriding the method's behavior.

Chapter 24

Chapter 24

1 One could call this pattern a "Teardown Method," but that name might be confused with the method used in Implicit Teardown (page 516).

1 One could call this pattern a "Teardown Method," but that name might be confused with the method used in Implicit Teardown (page 516).

2 The terminology of SUT becomes very confusing in this case because we cannot replace the SUT with a Test Double if it truly is the SUT. Strictly speaking, we are replacing the object that would normally be the SUT with respect to this test. Because we are actually verifying the behavior of the Parameterized Test, whatever normally plays the role of SUT for this test now becomes a DOC. (My head is starting to hurt just describing this; fortunately, it really isn't very complicated and will make a lot more sense when you actually try it out.)

2 The terminology of SUT becomes very confusing in this case because we cannot replace the SUT with a Test Double if it truly is the SUT. Strictly speaking, we are replacing the object that would normally be the SUT with respect to this test. Because we are actually verifying the behavior of the Parameterized Test, whatever normally plays the role of SUT for this test now becomes a DOC. (My head is starting to hurt just describing this; fortunately, it really isn't very complicated and will make a lot more sense when you actually try it out.)

3 This is because most members of the xUnit terminate the Test Method on the first failed assertion.

3 This is because most members of the xUnit terminate the Test Method on the first failed assertion.

Chapter 25

Chapter 25

1 See In-line Teardown for an explanation of what is wrong here.

1 See In-line Teardown for an explanation of what is wrong here.

2这假设我们从没有机场开始,并且希望以没有机场结束。如果我们只想删除这些特定的机场,我们不能使用Table Truncation Teardown

2 This assumes that we start with no airports and want to end with no airports. If we want to delete just these specific airports, we cannot use Table Truncation Teardown.

3请参阅内联拆卸以了解此处的错误原因。

3 See In-line Teardown for an explanation of what is wrong here.

第 26 章

Chapter 26

1 “如果它没坏,就不要改变它(即使是为了提高可测试性)”在这种情况下是一种常见的、尽管有些误导的约束。

1 "If it ain't broke, don't change it (even to improve the testability)" is a common, albeit somewhat misguided, constraint in these circumstances.

2我们将这些测试称为“双峰”或“多峰”,因为它们可以使用真实和虚假的 DOC 来运行。

2 We call these tests "bimodal" or "multimodal" because they can be run with both real and fake DOCs.

3主要区别在于 Singleton 只有一个实例,而 Registry 则不做这样的承诺。线程特定存储允许对象通过众所周知的对象访问“全局”数据,其中访问的数据特定于特定线程;同一对象可能会根据正在运行的线程检索不同的数据。

3 The main difference is that a Singleton has only a single instance, whereas a Registry makes no such promise. Thread-Specific Storage allows objects to access "global" data via a well-known object, where the data accessed is specific to a particular thread; the same object might retrieve different data depending on which thread is being run.

第 27 章

Chapter 27

1来自维基百科:参数通常也被称为参数,尽管参数更正确地被认为是在运行时调用子程序时分配给参数变量的实际值或引用。当讨论调用子程序的代码时,传递到子程序中的任何值或引用都是参数,代码中给出这些值或引用的位置是参数列表。当讨论子程序定义中的代码时,子程序参数列表中的变量是参数,而运行时参数的值是参数。

1 From Wikipedia: Parameters are also commonly referred to as arguments, although arguments are more properly thought of as the actual values or references assigned to the parameter variables when the subroutine is called at runtime. When discussing code that is calling into a subroutine, any values or references passed into the subroutine are the arguments, and the place in the code where these values or references are given is the parameter list. When discussing the code inside the subroutine definition, the variables in the subroutine's parameter list are the parameters, while the values of the parameters at runtime are the arguments.

附录 B

Appendix B

1 CORBA 是通用对象请求代理体系结构的缩写。此标准由对象管理组织定义。

1 CORBA is an acronym for Common Object Request Broker Architecture. This standard is defined by the Object Management Group.